Chapter 23: Dynamic Programming: The Core Method
23.01Intermediate5 topics17 min5 interactive animations

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

Recursion and divide-and-conquer fundamentalsGreedy algorithms and exchange arguments

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.

23.01.1

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.

Java
1static long calls;
2
3static long fib(int n) {
4 calls++;
5 if (n <= 1) return n;
6 return fib(n - 1) + fib(n - 2); // fib(n - 2) is recomputed inside fib(n - 1)
7}
8
9static long callsFor(int n) {
10 calls = 0;
11 fib(n);
12 return calls; // 15 for n = 5, 25 for 6, 67 for 8, 177 for 10
13}

Fibonacci: naive recursion tree

Interactive · edit the input

Your input

Try

Recursion tree · 1 call

fib(5)…
Running now
Call

Call fib(5).

Step 1 / 60
Java
1static long fib(int n) {
2 if (n <= 1) return n; // base cases
3 return fib(n - 1) + fib(n - 2); // two recursive calls
4}

Live state

1 calls

n

5

depth

1

No condition evaluated on this step

Call stack (top first)

  1. fib(5)
Execution trace (dry run table)Show 60 steps
StepLineStateWhat happens
1L1n=5 depth=1Call fib(5).
2L2n=5Not a base case.
3L3n=5fib(5) = fib(4) + fib(3). Call fib(4) first.
4L1n=4 depth=2Call fib(4).
5L2n=4Not a base case.
6L3n=4fib(4) = fib(3) + fib(2). Call fib(3) first.
7L1n=3 depth=3Call fib(3).
8L2n=3Not a base case.
9L3n=3fib(3) = fib(2) + fib(1). Call fib(2) first.
10L1n=2 depth=4Call fib(2).
11L2n=2Not a base case.
12L3n=2fib(2) = fib(1) + fib(0). Call fib(1) first.
13L1n=1 depth=5Call fib(1).
14L2n=1Base case: fib(1) = 1.
15L2n=1 return=1return 1.
16L3n=2 left=1fib(1) = 1. Now call fib(0).
17L1n=0 depth=5Call fib(0).
18L2n=0Base case: fib(0) = 0.
19L2n=0 return=0return 0.
20L3n=2 left=1 right=0 return=1fib(2) = 1 + 0 = 1.
21L3n=3 left=1fib(2) = 1. Now call fib(1).
22L1n=1 depth=4Call fib(1).
23L2n=1Base case: fib(1) = 1.
24L2n=1 return=1return 1.
25L3n=3 left=1 right=1 return=2fib(3) = 1 + 1 = 2.
26L3n=4 left=2fib(3) = 2. Now call fib(2).
27L1n=2 depth=3Call fib(2).
28L2n=2Not a base case.
29L3n=2fib(2) = fib(1) + fib(0). Call fib(1) first.
30L1n=1 depth=4Call fib(1).
31L2n=1Base case: fib(1) = 1.
32L2n=1 return=1return 1.
33L3n=2 left=1fib(1) = 1. Now call fib(0).
34L1n=0 depth=4Call fib(0).
35L2n=0Base case: fib(0) = 0.
36L2n=0 return=0return 0.
37L3n=2 left=1 right=0 return=1fib(2) = 1 + 0 = 1.
38L3n=4 left=2 right=1 return=3fib(4) = 2 + 1 = 3.
39L3n=5 left=3fib(4) = 3. Now call fib(3).
40L1n=3 depth=2Call fib(3).
41L2n=3Not a base case.
42L3n=3fib(3) = fib(2) + fib(1). Call fib(2) first.
43L1n=2 depth=3Call fib(2).
44L2n=2Not a base case.
45L3n=2fib(2) = fib(1) + fib(0). Call fib(1) first.
46L1n=1 depth=4Call fib(1).
47L2n=1Base case: fib(1) = 1.
48L2n=1 return=1return 1.
49L3n=2 left=1fib(1) = 1. Now call fib(0).
50L1n=0 depth=4Call fib(0).
51L2n=0Base case: fib(0) = 0.
52L2n=0 return=0return 0.
53L3n=2 left=1 right=0 return=1fib(2) = 1 + 0 = 1.
54L3n=3 left=1fib(2) = 1. Now call fib(1).
55L1n=1 depth=3Call fib(1).
56L2n=1Base case: fib(1) = 1.
57L2n=1 return=1return 1.
58L3n=3 left=1 right=1 return=2fib(3) = 1 + 1 = 2.
59L3n=5 left=3 right=2 return=5fib(5) = 3 + 2 = 5.
60L3return=5fib(5) = 5 with 15 calls. Notice how fib(3) and its subtrees repeat. Memoization removes this waste.
23.01.2

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.

Java
1// fewest coins for amount, plain recursion; returns -1 when impossible
2static int minCoins(int[] coins, int amount) {
3 if (amount == 0) return 0;
4 int best = Integer.MAX_VALUE;
5 for (int c : coins) {
6 if (c > amount) continue;
7 int sub = minCoins(coins, amount - c); // optimal answer for the smaller amount
8 if (sub != -1) best = Math.min(best, sub + 1);
9 }
10 return best == Integer.MAX_VALUE ? -1 : best;
11}

Coin change: fewest coins, or number of ways

Interactive · edit the input

Your input

Try

dp[a] = fewest coins

0a=0
∞a=1
∞a=2
∞a=3
∞a=4
∞a=5
∞a=6

last coin used

-a=0
-a=1
-a=2
-a=3
-a=4
-a=5
-a=6
Setup

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.

Step 1 / 15
Java
1static int minCoins(int[] coins, int amount) {
2 int INF = amount + 1;
3 int[] dp = new int[amount + 1]; Arrays.fill(dp, INF); dp[0] = 0; // dp[a] = fewest coins that make a
4 for (int a = 1; a <= amount; a++)
5 for (int c : coins)
6 if (c <= a && dp[a - c] + 1 < dp[a]) dp[a] = dp[a - c] + 1; // one coin c, then solve a - c
7 return dp[amount] >= INF ? -1 : dp[amount];
8}
9static long ways(int[] coins, int amount) {
10 long[] dp = new long[amount + 1]; dp[0] = 1; // one way to make 0: take nothing
11 for (int c : coins) // coins in the outer loop: each combination is counted once
12 for (int a = c; a <= amount; a++) dp[a] += dp[a - c];
13 return dp[amount];
14}

Live state

amount

6

coins

[1, 3, 4]

No condition evaluated on this step
Execution trace (dry run table)Show 15 steps
StepLineStateWhat happens
1L3amount=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.
2L6a=1 coin=1 candidate=1 dp[a]=1Coin 1: dp[0] + 1 = 1 beats the old value: dp[1] = 1.
3L6a=2 coin=1 candidate=2 dp[a]=2Coin 1: dp[1] + 1 = 2 beats the old value: dp[2] = 2.
4L6a=3 coin=1 candidate=3 dp[a]=3Coin 1: dp[2] + 1 = 3 beats the old value: dp[3] = 3.
5L6a=3 coin=3 candidate=1 dp[a]=1Coin 3: dp[0] + 1 = 1 beats the old value: dp[3] = 1.
6L6a=4 coin=1 candidate=2 dp[a]=2Coin 1: dp[3] + 1 = 2 beats the old value: dp[4] = 2.
7L6a=4 coin=3 candidate=2 dp[a]=2Coin 3: dp[1] + 1 = 2 does not improve dp[4] = 2.
8L6a=4 coin=4 candidate=1 dp[a]=1Coin 4: dp[0] + 1 = 1 beats the old value: dp[4] = 1.
9L6a=5 coin=1 candidate=2 dp[a]=2Coin 1: dp[4] + 1 = 2 beats the old value: dp[5] = 2.
10L6a=5 coin=3 candidate=3 dp[a]=2Coin 3: dp[2] + 1 = 3 does not improve dp[5] = 2.
11L6a=5 coin=4 candidate=2 dp[a]=2Coin 4: dp[1] + 1 = 2 does not improve dp[5] = 2.
12L6a=6 coin=1 candidate=3 dp[a]=3Coin 1: dp[5] + 1 = 3 beats the old value: dp[6] = 3.
13L6a=6 coin=3 candidate=2 dp[a]=2Coin 3: dp[3] + 1 = 2 beats the old value: dp[6] = 2.
14L6a=6 coin=4 candidate=3 dp[a]=2Coin 4: dp[2] + 1 = 3 does not improve dp[6] = 2.
15L7return=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.
23.01.3

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.

Java
1// number of paths from node 0 to node n in the DAG with edges i -> i+1 and i -> i+2
2static long countPaths(int n) {
3 long[] paths = new long[n + 1];
4 paths[0] = 1;
5 for (int node = 0; node < n; node++) { // nodes in topological order: increasing index
6 paths[node + 1] += paths[node]; // edge node -> node + 1
7 if (node + 2 <= n) paths[node + 2] += paths[node]; // edge node -> node + 2
8 }
9 return paths[n];
10}

Climbing stairs: a table, then two variables

Interactive · edit the input

Your input

Try

dp[i] = ways to reach step i

1i=0
1i=1
?i=2
?i=3
?i=4
?i=5
?i=6
?i=7
Setup

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.

Step 1 / 8
Java
1static long ways(int n) { // dp[i] = number of ways to reach step i with moves of 1 or 2
2 long[] dp = new long[n + 1]; dp[0] = 1; // one way to stand at the start: do nothing
3 if (n >= 1) dp[1] = 1;
4 for (int i = 2; i <= n; i++) dp[i] = dp[i - 1] + dp[i - 2]; // arrive from i-1 or from i-2
5 return dp[n];
6}
7static long waysRolling(int n) {
8 long prev2 = 1, prev1 = 1; // dp[0], dp[1]
9 for (int i = 2; i <= n; i++) { long cur = prev1 + prev2; prev2 = prev1; prev1 = cur; } // forget what is no longer needed
10 return prev1;
11}

Live state

n

7

No condition evaluated on this step
Execution trace (dry run table)Show 8 steps
StepLineStateWhat happens
1L2n=7To 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.
2L4i=2 dp[i-1]=1 dp[i-2]=1 dp[i]=2dp[2] = dp[1] + dp[0] = 1 + 1 = 2.
3L4i=3 dp[i-1]=2 dp[i-2]=1 dp[i]=3dp[3] = dp[2] + dp[1] = 2 + 1 = 3.
4L4i=4 dp[i-1]=3 dp[i-2]=2 dp[i]=5dp[4] = dp[3] + dp[2] = 3 + 2 = 5.
5L4i=5 dp[i-1]=5 dp[i-2]=3 dp[i]=8dp[5] = dp[4] + dp[3] = 5 + 3 = 8.
6L4i=6 dp[i-1]=8 dp[i-2]=5 dp[i]=13dp[6] = dp[5] + dp[4] = 8 + 5 = 13.
7L4i=7 dp[i-1]=13 dp[i-2]=8 dp[i]=21dp[7] = dp[6] + dp[5] = 13 + 8 = 21.
8L5return=2121 ways to climb 7 stairs. Time O(n); space O(n). This is the Fibonacci recurrence.
23.01.4

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.

ProblemStateNumber of states
Climbing stairsstep index in + 1
Fewest coinsamount aamount + 1
0/1 knapsack(items considered i, capacity left w)(n + 1)(W + 1)
Decode waysprefix length in + 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 input

Your input

Try

grid · 5 × 6

012345
0
item \ cap
0
1
2
3
4
1
none
0
0
0
0
0
2
#1 (1:15)
3
#2 (3:20)
4
#3 (4:30)
Cell being computedCell it readsOn the tracebackWrong: item reused
Setup

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).

Step 1 / 17
Java
1static int knapsack2D(int[] w, int[] v, int cap) {
2 int n = w.length; int[][] dp = new int[n + 1][cap + 1]; // dp[i][c]: best value from the first i items with capacity c
3 for (int i = 1; i <= n; i++)
4 for (int c = 0; c <= cap; c++) {
5 dp[i][c] = dp[i - 1][c]; // skip item i
6 if (c >= w[i - 1]) dp[i][c] = Math.max(dp[i][c], dp[i - 1][c - w[i - 1]] + v[i - 1]); // or take it
7 }
8 return dp[n][cap];
9}
10static int knapsack1D(int[] w, int[] v, int cap, boolean backwards) {
11 int[] dp = new int[cap + 1];
12 for (int i = 0; i < w.length; i++) {
13 if (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 i
14 else 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 }
16 return dp[cap];
17}

Live state

items

3

capacity

4

No condition evaluated on this step
Execution trace (dry run table)Show 17 steps
StepLineStateWhat happens
1L2items=3 capacity=4dp[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).
2L6item=1 c=0 skip=0 take=does not fit dp[i][c]=0Item 1 (weight 1) does not fit in capacity 0: copy the cell above (0).
3L6item=1 c=1 skip=0 take=15 dp[i][c]=15Capacity 1: skip item 1 → 0; take it → 0 + 15 = 15. Keep 15.
4L6item=1 c=2 skip=0 take=15 dp[i][c]=15Capacity 2: skip item 1 → 0; take it → 0 + 15 = 15. Keep 15.
5L6item=1 c=3 skip=0 take=15 dp[i][c]=15Capacity 3: skip item 1 → 0; take it → 0 + 15 = 15. Keep 15.
6L6item=1 c=4 skip=0 take=15 dp[i][c]=15Capacity 4: skip item 1 → 0; take it → 0 + 15 = 15. Keep 15.
7L6item=2 c=0 skip=0 take=does not fit dp[i][c]=0Item 2 (weight 3) does not fit in capacity 0: copy the cell above (0).
8L6item=2 c=1 skip=15 take=does not fit dp[i][c]=15Item 2 (weight 3) does not fit in capacity 1: copy the cell above (15).
9L6item=2 c=2 skip=15 take=does not fit dp[i][c]=15Item 2 (weight 3) does not fit in capacity 2: copy the cell above (15).
10L6item=2 c=3 skip=15 take=20 dp[i][c]=20Capacity 3: skip item 2 → 15; take it → 0 + 20 = 20. Keep 20.
11L6item=2 c=4 skip=15 take=35 dp[i][c]=35Capacity 4: skip item 2 → 15; take it → 15 + 20 = 35. Keep 35.
12L6item=3 c=0 skip=0 take=does not fit dp[i][c]=0Item 3 (weight 4) does not fit in capacity 0: copy the cell above (0).
13L6item=3 c=1 skip=15 take=does not fit dp[i][c]=15Item 3 (weight 4) does not fit in capacity 1: copy the cell above (15).
14L6item=3 c=2 skip=15 take=does not fit dp[i][c]=15Item 3 (weight 4) does not fit in capacity 2: copy the cell above (15).
15L6item=3 c=3 skip=20 take=does not fit dp[i][c]=20Item 3 (weight 4) does not fit in capacity 3: copy the cell above (20).
16L6item=3 c=4 skip=35 take=30 dp[i][c]=35Capacity 4: skip item 3 → 35; take it → 0 + 30 = 30. Keep 35.
17L8return=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.
23.01.5

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.

Java
1static int rob(int[] value) {
2 int n = value.length;
3 int[] best = new int[n + 1]; // best[i]: most loot from the first i houses
4 for (int i = 1; i <= n; i++) {
5 int skip = best[i - 1];
6 int take = (i >= 2 ? best[i - 2] : 0) + value[i - 1];
7 best[i] = Math.max(skip, take); // the last decision is skip or take
8 }
9 return best[n];
10}

House robber: take it or skip it

Interactive · edit the input

Your input

Try

house money

2h1
7h2
9h3
3h4
1h5

dp[i] = best from the first i houses

0none
2rob h1
?i=2
?i=3
?i=4
?i=5
Setup

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).

Step 1 / 6
Java
1static int rob(int[] a) { // no two adjacent houses
2 int n = a.length; int[] dp = new int[n + 1]; // dp[i] = best loot from the first i houses
3 dp[1] = a[0];
4 for (int i = 2; i <= n; i++)
5 dp[i] = Math.max(dp[i - 1], dp[i - 2] + a[i - 1]); // skip house i, or rob it and use the best before the neighbour
6 return dp[n];
7}

Live state

houses

5

No condition evaluated on this step
Execution trace (dry run table)Show 6 steps
StepLineStateWhat happens
1L3houses=5For 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).
2L5i=2 skip=2 rob=7 dp[i]=7House 2: skipping keeps 2; robbing gives dp[0] + 7 = 7. Best 7 (rob it).
3L5i=3 skip=7 rob=11 dp[i]=11House 3: skipping keeps 7; robbing gives dp[1] + 9 = 11. Best 11 (rob it).
4L5i=4 skip=11 rob=10 dp[i]=11House 4: skipping keeps 11; robbing gives dp[2] + 3 = 10. Best 11 (skip it).
5L5i=5 skip=11 rob=12 dp[i]=12House 5: skipping keeps 11; robbing gives dp[3] + 1 = 12. Best 12 (rob it).
6L6return=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.