Recognizing DP
How to recognise a dynamic programming problem: overlapping subproblems, optimal substructure, the DAG of states, designing the state and writing the transition function.
Learning goals
- Spot repeated subproblems in a recursion tree
- State optimal substructure for a problem and write its recurrence
- View states and transitions as a directed acyclic graph
- Choose a state that carries exactly the information the future needs
- Write a transition function from smaller states to a larger one
Prerequisites
Dynamic programming is plain recursion with one extra idea: if the same question is asked many times, answer it once and remember it. Recognising when that applies is the whole first step.
Overlapping subproblems
A recursion has **overlapping subproblems** when the same call is made many times with the same arguments. The Fibonacci recursion fib(n) = fib(n - 1) + fib(n - 2) is the classic: fib(5) calls fib(4) and fib(3), and fib(4) calls fib(3) again. Run the animation on n = 5 and 6 and watch fib(3) and its whole subtree reappear in the recursion tree.
The waste grows fast. The plain recursion makes 15 calls for n = 5, 25 for n = 6, 67 for n = 8 and 177 for n = 10, about 1.6 times more with every increment of n, while only n + 1 distinct values exist. A problem is a candidate for dynamic programming when the number of distinct subproblems is small (polynomial) but the plain recursion explores exponentially many calls.
1static long calls;23static long fib(int n) {4calls++;5if (n <= 1) return n;6return fib(n - 1) + fib(n - 2); // fib(n - 2) is recomputed inside fib(n - 1)7}89static long callsFor(int n) {10calls = 0;11fib(n);12return calls; // 15 for n = 5, 25 for 6, 67 for 8, 177 for 1013}
Fibonacci: naive recursion tree
Interactive · edit the inputRecursion tree · 1 call
Call fib(5).
1static long fib(int n) {2if (n <= 1) return n; // base cases3return fib(n - 1) + fib(n - 2); // two recursive calls4}
Live state
n
5
depth
1
Call stack (top first)
- fib(5)
Execution trace (dry run table)Show 60 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | n=5 depth=1 | Call fib(5). |
| 2 | L2 | n=5 | Not a base case. |
| 3 | L3 | n=5 | fib(5) = fib(4) + fib(3). Call fib(4) first. |
| 4 | L1 | n=4 depth=2 | Call fib(4). |
| 5 | L2 | n=4 | Not a base case. |
| 6 | L3 | n=4 | fib(4) = fib(3) + fib(2). Call fib(3) first. |
| 7 | L1 | n=3 depth=3 | Call fib(3). |
| 8 | L2 | n=3 | Not a base case. |
| 9 | L3 | n=3 | fib(3) = fib(2) + fib(1). Call fib(2) first. |
| 10 | L1 | n=2 depth=4 | Call fib(2). |
| 11 | L2 | n=2 | Not a base case. |
| 12 | L3 | n=2 | fib(2) = fib(1) + fib(0). Call fib(1) first. |
| 13 | L1 | n=1 depth=5 | Call fib(1). |
| 14 | L2 | n=1 | Base case: fib(1) = 1. |
| 15 | L2 | n=1 return=1 | return 1. |
| 16 | L3 | n=2 left=1 | fib(1) = 1. Now call fib(0). |
| 17 | L1 | n=0 depth=5 | Call fib(0). |
| 18 | L2 | n=0 | Base case: fib(0) = 0. |
| 19 | L2 | n=0 return=0 | return 0. |
| 20 | L3 | n=2 left=1 right=0 return=1 | fib(2) = 1 + 0 = 1. |
| 21 | L3 | n=3 left=1 | fib(2) = 1. Now call fib(1). |
| 22 | L1 | n=1 depth=4 | Call fib(1). |
| 23 | L2 | n=1 | Base case: fib(1) = 1. |
| 24 | L2 | n=1 return=1 | return 1. |
| 25 | L3 | n=3 left=1 right=1 return=2 | fib(3) = 1 + 1 = 2. |
| 26 | L3 | n=4 left=2 | fib(3) = 2. Now call fib(2). |
| 27 | L1 | n=2 depth=3 | Call fib(2). |
| 28 | L2 | n=2 | Not a base case. |
| 29 | L3 | n=2 | fib(2) = fib(1) + fib(0). Call fib(1) first. |
| 30 | L1 | n=1 depth=4 | Call fib(1). |
| 31 | L2 | n=1 | Base case: fib(1) = 1. |
| 32 | L2 | n=1 return=1 | return 1. |
| 33 | L3 | n=2 left=1 | fib(1) = 1. Now call fib(0). |
| 34 | L1 | n=0 depth=4 | Call fib(0). |
| 35 | L2 | n=0 | Base case: fib(0) = 0. |
| 36 | L2 | n=0 return=0 | return 0. |
| 37 | L3 | n=2 left=1 right=0 return=1 | fib(2) = 1 + 0 = 1. |
| 38 | L3 | n=4 left=2 right=1 return=3 | fib(4) = 2 + 1 = 3. |
| 39 | L3 | n=5 left=3 | fib(4) = 3. Now call fib(3). |
| 40 | L1 | n=3 depth=2 | Call fib(3). |
| 41 | L2 | n=3 | Not a base case. |
| 42 | L3 | n=3 | fib(3) = fib(2) + fib(1). Call fib(2) first. |
| 43 | L1 | n=2 depth=3 | Call fib(2). |
| 44 | L2 | n=2 | Not a base case. |
| 45 | L3 | n=2 | fib(2) = fib(1) + fib(0). Call fib(1) first. |
| 46 | L1 | n=1 depth=4 | Call fib(1). |
| 47 | L2 | n=1 | Base case: fib(1) = 1. |
| 48 | L2 | n=1 return=1 | return 1. |
| 49 | L3 | n=2 left=1 | fib(1) = 1. Now call fib(0). |
| 50 | L1 | n=0 depth=4 | Call fib(0). |
| 51 | L2 | n=0 | Base case: fib(0) = 0. |
| 52 | L2 | n=0 return=0 | return 0. |
| 53 | L3 | n=2 left=1 right=0 return=1 | fib(2) = 1 + 0 = 1. |
| 54 | L3 | n=3 left=1 | fib(2) = 1. Now call fib(1). |
| 55 | L1 | n=1 depth=3 | Call fib(1). |
| 56 | L2 | n=1 | Base case: fib(1) = 1. |
| 57 | L2 | n=1 return=1 | return 1. |
| 58 | L3 | n=3 left=1 right=1 return=2 | fib(3) = 1 + 1 = 2. |
| 59 | L3 | n=5 left=3 right=2 return=5 | fib(5) = 3 + 2 = 5. |
| 60 | L3 | return=5 | fib(5) = 5 with 15 calls. Notice how fib(3) and its subtrees repeat. Memoization removes this waste. |
Optimal substructure
**Optimal substructure** means the best answer to the whole problem is built from best answers to smaller subproblems. For fewest coins to make an amount, the last coin c in an optimal solution leaves amount - c, and the rest must itself be an optimal solution for amount - c, because otherwise swapping in a better one would improve the whole.
That argument gives the recurrence best(a) = 1 + min over coins c <= a of best(a - c), with best(0) = 0. Substructure is half of the recipe; overlapping subproblems is the other half. If subproblems did not repeat, the plain recursion would be fine; if substructure failed (the best answer to the whole needs a non-best answer to a part), the recurrence would give wrong answers.
Not every optimisation has the property. The longest simple path in a graph does not: the longest path from A to C through B does not consist of the longest path from A to B followed by anything, because those two parts might share vertices.
1// fewest coins for amount, plain recursion; returns -1 when impossible2static int minCoins(int[] coins, int amount) {3if (amount == 0) return 0;4int best = Integer.MAX_VALUE;5for (int c : coins) {6if (c > amount) continue;7int sub = minCoins(coins, amount - c); // optimal answer for the smaller amount8if (sub != -1) best = Math.min(best, sub + 1);9}10return best == Integer.MAX_VALUE ? -1 : best;11}
Coin change: fewest coins, or number of ways
Interactive · edit the inputdp[a] = fewest coins
last coin used
dp[a] is the fewest coins that make a. dp[0] = 0, everything else starts at ∞ (not reachable yet). For each amount try every coin c: use one c and add 1 to the best for a − c.
1static int minCoins(int[] coins, int amount) {2int INF = amount + 1;3int[] dp = new int[amount + 1]; Arrays.fill(dp, INF); dp[0] = 0; // dp[a] = fewest coins that make a4for (int a = 1; a <= amount; a++)5for (int c : coins)6if (c <= a && dp[a - c] + 1 < dp[a]) dp[a] = dp[a - c] + 1; // one coin c, then solve a - c7return dp[amount] >= INF ? -1 : dp[amount];8}9static long ways(int[] coins, int amount) {10long[] dp = new long[amount + 1]; dp[0] = 1; // one way to make 0: take nothing11for (int c : coins) // coins in the outer loop: each combination is counted once12for (int a = c; a <= amount; a++) dp[a] += dp[a - c];13return dp[amount];14}
Live state
amount
6
coins
[1, 3, 4]
Execution trace (dry run table)Show 15 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | amount=6 coins=[1, 3, 4] | dp[a] is the fewest coins that make a. dp[0] = 0, everything else starts at ∞ (not reachable yet). For each amount try every coin c: use one c and add 1 to the best for a − c. |
| 2 | L6 | a=1 coin=1 candidate=1 dp[a]=1 | Coin 1: dp[0] + 1 = 1 beats the old value: dp[1] = 1. |
| 3 | L6 | a=2 coin=1 candidate=2 dp[a]=2 | Coin 1: dp[1] + 1 = 2 beats the old value: dp[2] = 2. |
| 4 | L6 | a=3 coin=1 candidate=3 dp[a]=3 | Coin 1: dp[2] + 1 = 3 beats the old value: dp[3] = 3. |
| 5 | L6 | a=3 coin=3 candidate=1 dp[a]=1 | Coin 3: dp[0] + 1 = 1 beats the old value: dp[3] = 1. |
| 6 | L6 | a=4 coin=1 candidate=2 dp[a]=2 | Coin 1: dp[3] + 1 = 2 beats the old value: dp[4] = 2. |
| 7 | L6 | a=4 coin=3 candidate=2 dp[a]=2 | Coin 3: dp[1] + 1 = 2 does not improve dp[4] = 2. |
| 8 | L6 | a=4 coin=4 candidate=1 dp[a]=1 | Coin 4: dp[0] + 1 = 1 beats the old value: dp[4] = 1. |
| 9 | L6 | a=5 coin=1 candidate=2 dp[a]=2 | Coin 1: dp[4] + 1 = 2 beats the old value: dp[5] = 2. |
| 10 | L6 | a=5 coin=3 candidate=3 dp[a]=2 | Coin 3: dp[2] + 1 = 3 does not improve dp[5] = 2. |
| 11 | L6 | a=5 coin=4 candidate=2 dp[a]=2 | Coin 4: dp[1] + 1 = 2 does not improve dp[5] = 2. |
| 12 | L6 | a=6 coin=1 candidate=3 dp[a]=3 | Coin 1: dp[5] + 1 = 3 beats the old value: dp[6] = 3. |
| 13 | L6 | a=6 coin=3 candidate=2 dp[a]=2 | Coin 3: dp[3] + 1 = 2 beats the old value: dp[6] = 2. |
| 14 | L6 | a=6 coin=4 candidate=3 dp[a]=2 | Coin 4: dp[2] + 1 = 3 does not improve dp[6] = 2. |
| 15 | L7 | return=2 coins=[3, 3] | Fewest coins: 2. Following the last coin used from 6 down to 0 gives the coins [3, 3]. Other optimal combinations may exist; the traceback shows the one the table remembers. |
DAG interpretation
Think of every subproblem as a **node** and every dependency (the transition 'to solve this, I need that') as a **directed edge**. Because each subproblem depends on smaller ones, the graph has no cycles: it is a **directed acyclic graph** (DAG), and a valid evaluation order is any topological order of it.
Many problems become path problems on this graph. Climbing stairs with steps of 1 or 2 has nodes 0..n and edges i to i + 1 and i + 2; the number of ways is the **number of paths** from 0 to n, which for n = 7 is 21. Minimum-cost climbing and coin change ask for the **shortest path** from the start to the goal, with costs on edges or nodes; maximum-loot problems ask for the **longest path**, which is easy on a DAG.
This view tells you the running time too: dynamic programming visits each node once and each edge once, so the cost is the number of states plus the number of transitions.
1// number of paths from node 0 to node n in the DAG with edges i -> i+1 and i -> i+22static long countPaths(int n) {3long[] paths = new long[n + 1];4paths[0] = 1;5for (int node = 0; node < n; node++) { // nodes in topological order: increasing index6paths[node + 1] += paths[node]; // edge node -> node + 17if (node + 2 <= n) paths[node + 2] += paths[node]; // edge node -> node + 28}9return paths[n];10}
Climbing stairs: a table, then two variables
Interactive · edit the inputdp[i] = ways to reach step i
To reach step i you arrive from step i−1 (one step) or step i−2 (two steps), so the ways add up: dp[i] = dp[i−1] + dp[i−2]. The base cases are dp[0] = 1 (start) and dp[1] = 1.
1static long ways(int n) { // dp[i] = number of ways to reach step i with moves of 1 or 22long[] dp = new long[n + 1]; dp[0] = 1; // one way to stand at the start: do nothing3if (n >= 1) dp[1] = 1;4for (int i = 2; i <= n; i++) dp[i] = dp[i - 1] + dp[i - 2]; // arrive from i-1 or from i-25return dp[n];6}7static long waysRolling(int n) {8long prev2 = 1, prev1 = 1; // dp[0], dp[1]9for (int i = 2; i <= n; i++) { long cur = prev1 + prev2; prev2 = prev1; prev1 = cur; } // forget what is no longer needed10return prev1;11}
Live state
n
7
Execution trace (dry run table)Show 8 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | n=7 | To reach step i you arrive from step i−1 (one step) or step i−2 (two steps), so the ways add up: dp[i] = dp[i−1] + dp[i−2]. The base cases are dp[0] = 1 (start) and dp[1] = 1. |
| 2 | L4 | i=2 dp[i-1]=1 dp[i-2]=1 dp[i]=2 | dp[2] = dp[1] + dp[0] = 1 + 1 = 2. |
| 3 | L4 | i=3 dp[i-1]=2 dp[i-2]=1 dp[i]=3 | dp[3] = dp[2] + dp[1] = 2 + 1 = 3. |
| 4 | L4 | i=4 dp[i-1]=3 dp[i-2]=2 dp[i]=5 | dp[4] = dp[3] + dp[2] = 3 + 2 = 5. |
| 5 | L4 | i=5 dp[i-1]=5 dp[i-2]=3 dp[i]=8 | dp[5] = dp[4] + dp[3] = 5 + 3 = 8. |
| 6 | L4 | i=6 dp[i-1]=8 dp[i-2]=5 dp[i]=13 | dp[6] = dp[5] + dp[4] = 8 + 5 = 13. |
| 7 | L4 | i=7 dp[i-1]=13 dp[i-2]=8 dp[i]=21 | dp[7] = dp[6] + dp[5] = 13 + 8 = 21. |
| 8 | L5 | return=21 | 21 ways to climb 7 stairs. Time O(n); space O(n). This is the Fibonacci recurrence. |
State design
The **state** is the set of values that identifies a subproblem. A good state has two properties. It is **sufficient**: it contains everything the future decisions depend on, and nothing about the past beyond that. And it is **small**: few enough distinct values for the table to fit. For the 0/1 knapsack, the state is (number of items considered, capacity left): the best value for the remaining items depends only on those two numbers, not on which items were taken already.
Common choices: a prefix length i for one sequence, a pair (i, j) of prefix lengths for two sequences, an amount or capacity, a subset as a bit mask, and an extra flag when a rule depends on the previous choice. The next lessons define the state before anything else: when a DP goes wrong, the state is the first suspect.
| Problem | State | Number of states |
|---|---|---|
| Climbing stairs | step index i | n + 1 |
| Fewest coins | amount a | amount + 1 |
| 0/1 knapsack | (items considered i, capacity left w) | (n + 1)(W + 1) |
| Decode ways | prefix length i | n + 1 |
| Longest common subsequence | (prefix of A, prefix of B) | (m + 1)(n + 1) |
| Travelling salesman (exact) | (visited set, current city) | 2^n × n |
0/1 knapsack: 2D table, and why 1D must run backwards
Interactive · edit the inputgrid · 5 × 6
dp[i][c] is the best value from the first i items with capacity c. Row 0 is all zeros. Each cell either skips item i (copy the cell above) or takes it (value + the cell above-left by the item's weight).
1static int knapsack2D(int[] w, int[] v, int cap) {2int n = w.length; int[][] dp = new int[n + 1][cap + 1]; // dp[i][c]: best value from the first i items with capacity c3for (int i = 1; i <= n; i++)4for (int c = 0; c <= cap; c++) {5dp[i][c] = dp[i - 1][c]; // skip item i6if (c >= w[i - 1]) dp[i][c] = Math.max(dp[i][c], dp[i - 1][c - w[i - 1]] + v[i - 1]); // or take it7}8return dp[n][cap];9}10static int knapsack1D(int[] w, int[] v, int cap, boolean backwards) {11int[] dp = new int[cap + 1];12for (int i = 0; i < w.length; i++) {13if (backwards) for (int c = cap; c >= w[i]; c--) dp[c] = Math.max(dp[c], dp[c - w[i]] + v[i]); // right to left: dp[c - w] still excludes item i14else for (int c = w[i]; c <= cap; c++) dp[c] = Math.max(dp[c], dp[c - w[i]] + v[i]); // left to right: item i may be taken again (a bug for 0/1)15}16return dp[cap];17}
Live state
items
3
capacity
4
Execution trace (dry run table)Show 17 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | items=3 capacity=4 | dp[i][c] is the best value from the first i items with capacity c. Row 0 is all zeros. Each cell either skips item i (copy the cell above) or takes it (value + the cell above-left by the item's weight). |
| 2 | L6 | item=1 c=0 skip=0 take=does not fit dp[i][c]=0 | Item 1 (weight 1) does not fit in capacity 0: copy the cell above (0). |
| 3 | L6 | item=1 c=1 skip=0 take=15 dp[i][c]=15 | Capacity 1: skip item 1 → 0; take it → 0 + 15 = 15. Keep 15. |
| 4 | L6 | item=1 c=2 skip=0 take=15 dp[i][c]=15 | Capacity 2: skip item 1 → 0; take it → 0 + 15 = 15. Keep 15. |
| 5 | L6 | item=1 c=3 skip=0 take=15 dp[i][c]=15 | Capacity 3: skip item 1 → 0; take it → 0 + 15 = 15. Keep 15. |
| 6 | L6 | item=1 c=4 skip=0 take=15 dp[i][c]=15 | Capacity 4: skip item 1 → 0; take it → 0 + 15 = 15. Keep 15. |
| 7 | L6 | item=2 c=0 skip=0 take=does not fit dp[i][c]=0 | Item 2 (weight 3) does not fit in capacity 0: copy the cell above (0). |
| 8 | L6 | item=2 c=1 skip=15 take=does not fit dp[i][c]=15 | Item 2 (weight 3) does not fit in capacity 1: copy the cell above (15). |
| 9 | L6 | item=2 c=2 skip=15 take=does not fit dp[i][c]=15 | Item 2 (weight 3) does not fit in capacity 2: copy the cell above (15). |
| 10 | L6 | item=2 c=3 skip=15 take=20 dp[i][c]=20 | Capacity 3: skip item 2 → 15; take it → 0 + 20 = 20. Keep 20. |
| 11 | L6 | item=2 c=4 skip=15 take=35 dp[i][c]=35 | Capacity 4: skip item 2 → 15; take it → 15 + 20 = 35. Keep 35. |
| 12 | L6 | item=3 c=0 skip=0 take=does not fit dp[i][c]=0 | Item 3 (weight 4) does not fit in capacity 0: copy the cell above (0). |
| 13 | L6 | item=3 c=1 skip=15 take=does not fit dp[i][c]=15 | Item 3 (weight 4) does not fit in capacity 1: copy the cell above (15). |
| 14 | L6 | item=3 c=2 skip=15 take=does not fit dp[i][c]=15 | Item 3 (weight 4) does not fit in capacity 2: copy the cell above (15). |
| 15 | L6 | item=3 c=3 skip=20 take=does not fit dp[i][c]=20 | Item 3 (weight 4) does not fit in capacity 3: copy the cell above (20). |
| 16 | L6 | item=3 c=4 skip=35 take=30 dp[i][c]=35 | Capacity 4: skip item 3 → 35; take it → 0 + 30 = 30. Keep 35. |
| 17 | L8 | return=35 items=[#1, #2] | Best value 35. Traceback from the bottom-right cell: whenever a cell differs from the one above it, that item was taken (items [#1, #2]), and we move left by its weight. |
Transition function
The **transition function** (or recurrence) says how to compute a state from smaller ones. In the house-robber problem, best(i) is the most loot from the first i houses. The i-th house is either skipped, leaving best(i - 1), or robbed, which forces skipping its neighbour and gives best(i - 2) + value(i). So best(i) = max(best(i - 1), best(i - 2) + value(i)).
Write the transition as an enumeration of the **last decision**: every optimal solution made some final choice, and each choice leads to a smaller state plus a known gain or cost. List all the choices, take the best, and the recurrence follows. In the “Classic” preset (2, 7, 9, 3, 1) the table ends at 12, and the animation shows each cell as the larger of its two options.
1static int rob(int[] value) {2int n = value.length;3int[] best = new int[n + 1]; // best[i]: most loot from the first i houses4for (int i = 1; i <= n; i++) {5int skip = best[i - 1];6int take = (i >= 2 ? best[i - 2] : 0) + value[i - 1];7best[i] = Math.max(skip, take); // the last decision is skip or take8}9return best[n];10}
House robber: take it or skip it
Interactive · edit the inputhouse money
dp[i] = best from the first i houses
For the first i houses, the best loot either skips house i (best of the first i−1) or robs it (its money plus the best of the first i−2, because the neighbour is off limits).
1static int rob(int[] a) { // no two adjacent houses2int n = a.length; int[] dp = new int[n + 1]; // dp[i] = best loot from the first i houses3dp[1] = a[0];4for (int i = 2; i <= n; i++)5dp[i] = Math.max(dp[i - 1], dp[i - 2] + a[i - 1]); // skip house i, or rob it and use the best before the neighbour6return dp[n];7}
Live state
houses
5
Execution trace (dry run table)Show 6 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | houses=5 | For the first i houses, the best loot either skips house i (best of the first i−1) or robs it (its money plus the best of the first i−2, because the neighbour is off limits). |
| 2 | L5 | i=2 skip=2 rob=7 dp[i]=7 | House 2: skipping keeps 2; robbing gives dp[0] + 7 = 7. Best 7 (rob it). |
| 3 | L5 | i=3 skip=7 rob=11 dp[i]=11 | House 3: skipping keeps 7; robbing gives dp[1] + 9 = 11. Best 11 (rob it). |
| 4 | L5 | i=4 skip=11 rob=10 dp[i]=11 | House 4: skipping keeps 11; robbing gives dp[2] + 3 = 10. Best 11 (skip it). |
| 5 | L5 | i=5 skip=11 rob=12 dp[i]=12 | House 5: skipping keeps 11; robbing gives dp[3] + 1 = 12. Best 12 (rob it). |
| 6 | L6 | return=12 houses=[1, 3, 5] | Maximum loot 12. Tracing the take/skip decisions backwards from house 5 gives the houses [1, 3, 5]. |
Practice ladder
Now try it yourself
Easy
- Fibonacci Number
- Climbing Stairs
Medium
- Coin Change
- House Robber
Advanced
- Partition Equal Subset Sum
- Longest Increasing Subsequence
Key takeaways
- Dynamic programming applies when a recursion has few distinct subproblems but many repeated calls.
- It needs optimal substructure: an optimal solution contains optimal solutions of its parts.
- States and transitions form a DAG; paths in it count ways, shortest paths minimise, longest paths maximise.
- A state must carry all information the future needs, and be small enough to tabulate.
- The transition enumerates the last decision and takes the best.