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).
Top‑down (memoization):
Bottom‑up (tabulation):
Rule of thumb: start top‑down to validate the recurrence; convert to bottom‑up for performance/space when the order is clear.
1D DP: linear index (
i) or value (sum); examples: climbing stairs, house robber, LIS (tails).
2D DP: pairs like (
i, j) for substrings or two sequences; examples: edit distance, LCS, grid paths.
Bitmask DP: small
n (≤20–22) with subset state mask; examples: TSP/style pathing, assignment, subset convolution. Complexity ~ O(n 2^n) or O(2^n n^2).
Pick the smallest state that makes transitions local; then consider rolling arrays (1D from 2D) when only prior row/diagonal is needed.
0/1 knapsack: each item once;
dp[w] = max(dp[w], dp[w-w_i] + v_i) iterating w downward.
Unbounded knapsack / coin change: infinite copies; iterate
w upward or loop items outside.
Bounded knapsack: limited copies; split by binary lifting (1,2,4,...) then treat as 0/1.
Multi‑dimensional knapsack: multiple capacities (weight, volume); state grows to 2D/3D.
Space tips: 1D rolling for 0/1 and unbounded; be mindful of iteration direction (down for 0/1, up for unbounded) to avoid reuse bugs.