Master the art of smart recursion with memory
DP is "smart recursion with memory."
You solve a problem by splitting it into smaller subproblems, but you never solve the same subproblem twice—you cache (memoize) results or fill a table (tabulation). This turns many exponential brute-force searches into polynomial-time solutions.
Two properties make DP work:
If both hold, DP is a strong candidate.
Greedy makes locally best choices—great when it's provably correct, but it can be fooled by problems where a local choice ruins the global optimum.
Time ≈ #states × avg transitions, Space ≈ #states.
If you see any of these, think DP:
i
, pairs (i, j)
, or ranges [l, r]
.If none of these fit and a correct greedy exists, DP might be overkill.
Define the state.
What minimal info makes the next decision local? e.g., i
(index), (i, j)
(substring), mask
(picked set), node
(tree), plus any small "mode" flags.
Write the recurrence (transitions). Express the answer for a state from smaller states. Decide whether you min, max, or sum results.
Base cases. Trivial starts (empty prefix, zero capacity, single char, etc.).
Order of evaluation.
Compute answer location.
Is it dp[n]
, dp[n][m]
, dp[0][n-1]
, or an aggregate over states?
Complexity & space tweaks.
Climbing stairs (count ways):
Ways to reach step i
= ways to reach i-1
+ ways to reach i-2
.
(Overlapping subproblems, sum transition, base: step 0 → 1 way.)
Coin change (min coins):
Best for amount x
= 1 + min(best[x - coin] for coin)
.
(Min transition; base: amount 0 → 0 coins.)
Edit distance (two strings):
dp[i][j]
= min of delete, insert, replace from smaller prefixes—
or carry dp[i-1][j-1]
if chars match.
(2D state, min transition.)
Burst balloons / matrix chain (interval DP):
dp[l][r]
= best split over k in (l..r-1)
plus local cost.
(Range state; try all split points.)
Think of each state as a node, each transition as a directed edge to smaller states.
DP = longest/shortest path/count of paths on this implicit DAG computed in a topological order (or via memoized DFS).