Dynamic Programming
Solve a problem by defining subproblems whose answers are reused, so exponential recursion collapses to polynomial time.
Overview
Dynamic programming (DP) is a method for problems that can be broken into subproblems that repeat. A naive recursion solves the same subproblem millions of times; DP solves each distinct subproblem once, stores the answer, and reuses it. The name is historical (Bellman, 1950s) and means nothing literal — think of it as "recursion with a lookup table".
Two properties must hold. Overlapping subproblems: the recursion tree contains the same node many times (Fibonacci computes fib(3) exponentially often). Optimal substructure: an optimal solution to the whole is built from optimal solutions to parts (the shortest path from A to C through B contains the shortest path from A to B). Without the first, DP gives no speedup over divide and conquer; without the second, a table of sub-answers cannot be combined into the right answer.
Every DP is fully specified by a single framework: State → Transition → Base case → Dependency order → Table → Optimization. The state names a subproblem with a small tuple of integers; the transition is a formula computing a state from smaller states; base cases anchor the recursion; the dependency order is a topological order of the implicit DAG of states; the table is where answers live; optimization means shrinking the table (often a 2D table to one or two rows) once the dependency structure is understood.
DP appears as its own topic family here — Memoization (Top-Down DP) and Tabulation (Bottom-Up DP) are the two execution styles, and 1D (Linear) DP, 2D (Two-Sequence) DP, Grid DP, Knapsack DP, Subsequence DP, Interval (Range) DP, State Machine DP, Tree DP, Bitmask DP, Digit DP and DP on DAGs are the shapes the state space takes in practice. Classic problems: Fibonacci Numbers, Climbing Stairs, House Robber, Coin Change, 0/1 Knapsack, Unbounded Knapsack, Longest Increasing Subsequence, Longest Common Subsequence, Edit Distance, Matrix Chain Multiplication, Kadane's Algorithm.
Intuition
A mental model before the formal terms.
Picture filling in a crossword where every clue is "add the two answers above me". Doing it by pure recursion means re-deriving the entire top of the grid each time you look at a cell. Writing answers into the grid as you go means each cell costs one addition, and the whole thing costs the number of cells.
Another picture: the recursion tree of fib(50) has about 2^50 nodes but only 51 distinct labels. Collapse every node with the same label into one and the tree becomes a small DAG with 51 vertices and 100 edges. DP is nothing more than evaluating that DAG once, in an order where every vertex's inputs are ready before it is.
The hard part of DP is never the code — a finished DP is three to ten lines. The hard part is choosing what to write on each cell: the state. A good state is the smallest description of "where am I" that still lets you decide the future without looking at the past.
How it works
- 1. State. Ask: "after making some decisions, what is the minimum information I need to finish optimally?" Encode it as a tuple of small integers, e.g.
dp[i]= best answer using the firstiitems,dp[i][j]= answer for prefixiof A and prefixjof B,dp[mask][v]= answer having visited setmask, standing atv. The number of distinct states is the table size, which bounds your time. - 2. Transition. Write the recurrence: how does
dp[state]follow from strictly "smaller" states? Enumerate the last decision: "the last element is either taken or skipped", "the last cut is at position k", "the last character matches or not".dp[state] = best over choices of (cost of choice + dp[smaller state]). - 3. Base cases. The states with no decisions left:
dp[0] = 0,dp[i][0] = i,dp[1 << start][start] = 0. Also decide what "impossible" means (-inf,+inf,0ways,false) so that transitions from impossible states stay impossible. - 4. Dependency order. The states form a DAG (transitions point to smaller states). Top-down Memoization (Top-Down DP) evaluates it lazily via recursion; bottom-up Tabulation (Bottom-Up DP) walks it in a topological order — increasing
i, increasing interval length, increasing popcount ofmask, post-order of a tree. If you cannot describe such an order, the state definition is circular and must be fixed. - 5. Table. Allocate
dpwith a slot per state (array, 2D array, hash map for sparse states). Fill in dependency order. The answer is one cell (dp[n]), or an aggregate over cells (max over i of dp[i]for Longest Increasing Subsequence). - 6. Optimization. If
dp[i][*]depends only ondp[i-1][*], keep two rows (or one row iterated in the right direction, as in 0/1 Knapsack). If only a fixed window of earlier states matters, keepkvariables. Monotonic-queue, convex-hull, divide-and-conquer and Knuth optimizations speed up specific transition shapes, but space reduction is the one you must always consider.
Why it works
Correctness follows from optimal substructure by induction over the dependency order: assuming every smaller state holds the true optimum, the transition considers every possible last decision and picks the best, so the current state also holds the true optimum. The base cases anchor the induction.
Efficiency follows from overlapping subproblems: total work = (number of states) × (work per transition). For Longest Common Subsequence that is n·m states × O(1) = O(nm), versus the 2^n leaves of the naive recursion. The exponential blowup existed only because the same states were recomputed.
The framework is the same for counting (replace max with sum), feasibility (replace max with OR), and optimization (max/min). Only the combining operator changes; states, transitions and order are identical. That is why one mental model covers "how many ways", "is it possible" and "what is the minimum cost".
Recognition
How to tell a problem wants this.
- The question asks for "the number of ways", "the minimum cost / fewest steps", "the maximum value / longest length", or "is it possible to…" — and a greedy choice can be shown to fail with a small counterexample.
- A brute-force recursion is easy to write but makes the same recursive call with the same arguments many times.
- Decisions are made in sequence (left to right over an array, character by character, item by item) and the future depends on a small summary of the past (an index, a remaining capacity, the previous choice).
- Constraint sizes are a strong hint:
n ≤ 5000suggestsO(n²)states,n ≤ 500with two sequences suggestsO(n·m),n ≤ 20suggests bitmask statesO(2^n·n),n ≤ 100intervals suggestsO(n³),N ≤ 10^18with a digit condition suggests Digit DP. - Keywords: "subsequence", "partition", "non-adjacent", "at most k transactions", "contiguous" (though contiguous max sum is Kadane's Algorithm), "minimum edits", "palindromic", "ways to tile/climb/decode".
Interactive visualization
Play, step, change the input. ← → and space work too.
Showing the closely related Fibonacci Numbers visualization.
| n | fib(n) |
|---|
1fib(n):2 if n <= 1: return n3 if n in memo: return memo[n]4 memo[n] = fib(n-1) + fib(n-2)5 return memo[n]Pseudocode
1# The DP framework, applied to "minimum coins to make amount A" (coin-change)2# State: dp[a] = fewest coins summing to a (a in 0..A)3# Transition: dp[a] = min over coins c <= a of 1 + dp[a - c]4# Base: dp[0] = 0; dp[a] = INF until proven reachable5# Order: increasing a (every a - c is smaller than a)6# Table: one array of size A + 17# Optimize: nothing to shrink; the table already is one row8dp = [INF] * (A + 1); dp[0] = 09for a in 1..A:10 for c in coins:11 if c <= a and dp[a - c] + 1 < dp[a]: dp[a] = dp[a - c] + 112return dp[A] if dp[A] != INF else -1Implementations
1# Representative problem: minimum number of coins to make amount (coin change)2# State: dp[a] = fewest coins summing to a. Transition: dp[a] = 1 + min(dp[a - c]).3def min_coins(coins: list[int], amount: int) -> int:41 · Table and sentinel5 INF = float("inf") # inf + 1 == inf, comparisons stay correct6 dp = [INF] * (amount + 1)72 · Base case8 dp[0] = 0 # zero coins make amount 093 · Fill in dependency order10 for a in range(1, amount + 1): # every a - c < a is already final114 · Transition over the last coin12 for c in coins:13 if c <= a and dp[a - c] + 1 < dp[a]:14 dp[a] = dp[a - c] + 1155 · Read the answer16 return -1 if dp[amount] == INF else int(dp[amount])17 18 19if __name__ == "__main__":20 print(min_coins([1, 2, 5], 11)) # 3 (5 + 5 + 1)21 print(min_coins([2], 3)) # -1- This is the representative example for the whole DP framework: state
dp[a], transition over the last coin, basedp[0] = 0, increasing-aorder, a one-row table. float("inf")is the sentinel;inf + 1 == infand it compares greater than any int, so the min logic needs no guard.[INF] * (amount + 1)is fine for a 1D table because floats are immutable — the aliasing trap only bites with nested lists.range(1, amount + 1)visits amounts in increasing order; everydp[a - c]read is already final.int(dp[amount])converts the float-typed cell back to anintfor the caller.
- Python ints never overflow, so counting variants need no
long long; only a modulus if the problem asks for one. - A list-of-floats table is slightly slower than ints;
INF = amount + 1(an int upper bound) is a common competitive trick. - This is the bottom-up form;
functools.lru_cachegives the top-down form in two lines (see alternative).
- Using
0as the "unreachable" sentinel —dp[0]is legitimately 0 and the min test breaks. - Returning
dp[amount]as a float (3.0) when anintis expected. - Iterating
for a in range(amount)and losing the last cell.
- Sentinel for "unreachable": C++ needs a finite
INF(1e9) small enough thatINF + 1does not overflowint; JS/TS and Python have a trueInfinity/float("inf")that absorbs additions. - Counting variants overflow in C++ (
intat 2^31,long longat 2^63) and lose precision in JS/TS beyond 2^53; Python ints are arbitrary precision. - Table allocation:
std::vector<int>(n, INF)vsnew Array(n).fill(INF)(a barenew Array(n)is sparse) vs[INF] * n.
Complexity
Typical families: 1D O(n), two-sequence O(nm), interval O(n³), bitmask O(2^n·n²), tree O(n). The exponential naive recursion becomes polynomial because each state is evaluated once.
Compare growth rates in the Complexity Explorer →When to use — and when not to
- Optimization, counting or feasibility over sequential decisions where greedy fails and brute force repeats work.
- The state space is small enough to enumerate: roughly ≤ 10^7–10^8 states × transition cost fits in a typical time limit.
- The problem can be phrased as "best answer for a prefix / suffix / interval / subset / subtree".
- A greedy exchange argument holds (e.g. Activity Selection, Fractional Knapsack) — greedy is simpler and faster.
- The state needs to remember an unbounded amount of history (the full set of visited items with n = 1000) — the table is too large; look for a different formulation or a different technique.
- Subproblems do not overlap (merge sort, quick sort) — that is Divide and Conquer; a memo table adds cost without benefit.
- The dependency graph has cycles (shortest paths with arbitrary weights) — use Dijkstra's Algorithm or Bellman-Ford rather than a DP table, or note that Bellman-Ford is DP over "number of edges used".
Alternatives
Common mistakes
- Wrong state — missing a dimension (forgetting "holding a stock or not" in stock problems, forgetting "which item index" in knapsack) so the transition silently depends on information the state does not carry.
- Missing or wrong base case —
dp[0]for "zero items" or "empty string" is usually0,1(one way to make the empty selection),true, orINF; getting it wrong shifts every answer. - Wrong iteration order — reading
dp[i-1][j]before it is written, or iterating capacity upward in 0/1 knapsack so an item is used twice. - Recomputing without a memo — a correct recurrence run as plain recursion is still exponential; the table is the whole point.
- Using
0as the sentinel for "impossible" when0is a legal value, so impossible states leak into real answers. - Integer overflow in counting DPs — take the modulus at every addition when the problem asks for
mod 10^9+7. - Off-by-one between "first
iitems" (indices0..i-1) and "itemi"; be explicit which convention the state uses.
Interview patterns
- Recognize the family from the state: one index → 1D (Linear) DP; two sequence indices → 2D (Two-Sequence) DP; row/col → Grid DP; index + capacity → Knapsack DP;
[l, r]→ Interval (Range) DP; index + small status → State Machine DP; subset mask → Bitmask DP; node → Tree DP. - Write the brute-force recursion first, then add a memo (Memoization (Top-Down DP)), then (if asked) convert to a table (Tabulation (Bottom-Up DP)) and reduce space.
- Reconstruct the actual solution (not just its value) by storing the argmax choice per state and walking back from the answer.
- Count with modular arithmetic; find minimum with
INFsentinels; check feasibility with booleans.
- Coin ChangeIntermediate