Chapter 23: Dynamic Programming: The Core Method
23.05Intermediate6 topics19 min6 interactive animations

Canonical beginner DP

The six beginner dynamic programming problems: climbing stairs, Fibonacci, house robber, minimum-cost climbing, decode ways and coin change.

Learning goals

  • Solve each of the six problems from a state and transition, not from memory
  • See that climbing stairs and Fibonacci are one recurrence
  • Apply the take-or-skip pattern in house robber
  • Count versus optimise with the same table shape in decode ways and coin change

Prerequisites

The DP workflowBottom-up tabulation

These six problems are the standard warm-up because each one introduces one idea. Solving them from the workflow, rather than recalling answers, is the skill that carries over.

23.05.1

Climbing stairs

**State:** ways[i] is the number of ways to reach step i. **Transition:** the last move was a 1-step from i - 1 or a 2-step from i - 2, so ways[i] = ways[i - 1] + ways[i - 2]. **Base:** ways[0] = ways[1] = 1. **Answer:** ways[n]. The “n = 7” presets give 21, and “n = 12” gives 233.

The count for n = 45 is 1 836 311 903, which still fits in int, but n = 46 gives 2 971 215 073, which does not; long lasts much longer, so for large n use long or BigInteger, or return the result modulo a given number.

Java
1static long climbStairs(int n) {
2 long a = 1, b = 1; // ways to reach steps i - 1 and i
3 for (int i = 2; i <= n; i++) { long c = a + b; a = b; b = c; }
4 return b;
5}

Best

O(n)

Average

O(n)

Worst

O(n)

Space

O(1)

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

Fibonacci

Fibonacci is climbing stairs with different base cases: F(0) = 0, F(1) = 1, and F(n) = F(n - 1) + F(n - 2). The naive recursion takes exponential time; memoization or a loop takes O(n). The animation shows the memoised version computing F(10) = 55 in 19 calls instead of 177.

long holds F(92) = 7 540 113 804 746 346 429 but overflows at F(93); for larger indices use BigInteger, or fast doubling for O(log n) multiplications. Prefer the loop to recursion for any value of n that is not tiny.

Java
1static long fibIterative(int n) {
2 long a = 0, b = 1; // F(i) and F(i + 1)
3 for (int i = 0; i < n; i++) { long c = a + b; a = b; b = c; }
4 return a;
5}
6
7static java.math.BigInteger fibBig(int n) {
8 java.math.BigInteger a = java.math.BigInteger.ZERO, b = java.math.BigInteger.ONE;
9 for (int i = 0; i < n; i++) { java.math.BigInteger c = a.add(b); a = b; b = c; }
10 return a;
11}

Fibonacci with memoization

Interactive · edit the input

Your input

Try

Recursion tree · 1 call

fib(6)…
Running now
Call

Call fib(6).

Step 1 / 52
Java
1static long[] memo = new long[100];
2static long fib(int n) {
3 if (n <= 1) return n; // base cases
4 if (memo[n] != 0) return memo[n]; // already solved: reuse it
5 return memo[n] = fib(n - 1) + fib(n - 2); // solve once, remember
6}

Live state

1 calls

n

6

depth

1

No condition evaluated on this step

Call stack (top first)

  1. fib(6)
Execution trace (dry run table)Show 52 steps
StepLineStateWhat happens
1L2n=6 depth=1Call fib(6).
2L3n=6Not a base case.
3L4n=6 memo[n]=0memo[6] is empty, so compute it.
4L5n=6fib(6) = fib(5) + fib(4). Call fib(5) first.
5L2n=5 depth=2Call fib(5).
6L3n=5Not a base case.
7L4n=5 memo[n]=0memo[5] is empty, so compute it.
8L5n=5fib(5) = fib(4) + fib(3). Call fib(4) first.
9L2n=4 depth=3Call fib(4).
10L3n=4Not a base case.
11L4n=4 memo[n]=0memo[4] is empty, so compute it.
12L5n=4fib(4) = fib(3) + fib(2). Call fib(3) first.
13L2n=3 depth=4Call fib(3).
14L3n=3Not a base case.
15L4n=3 memo[n]=0memo[3] is empty, so compute it.
16L5n=3fib(3) = fib(2) + fib(1). Call fib(2) first.
17L2n=2 depth=5Call fib(2).
18L3n=2Not a base case.
19L4n=2 memo[n]=0memo[2] is empty, so compute it.
20L5n=2fib(2) = fib(1) + fib(0). Call fib(1) first.
21L2n=1 depth=6Call fib(1).
22L3n=1Base case: fib(1) = 1.
23L3n=1 return=1return 1.
24L5n=2 left=1fib(1) = 1. Now call fib(0).
25L2n=0 depth=6Call fib(0).
26L3n=0Base case: fib(0) = 0.
27L3n=0 return=0return 0.
28L5n=2 left=1 right=0 return=1fib(2) = 1 + 0 = 1. Store it in memo[2].
29L5n=3 left=1fib(2) = 1. Now call fib(1).
30L2n=1 depth=5Call fib(1).
31L3n=1Base case: fib(1) = 1.
32L3n=1 return=1return 1.
33L5n=3 left=1 right=1 return=2fib(3) = 1 + 1 = 2. Store it in memo[3].
34L5n=4 left=2fib(3) = 2. Now call fib(2).
35L2n=2 depth=4Call fib(2).
36L3n=2Not a base case.
37L4n=2 memo[n]=1memo[2] is already 1. No need to recurse again.
38L4n=2 return=1Cache hit: return 1 immediately. This whole subtree is skipped.
39L5n=4 left=2 right=1 return=3fib(4) = 2 + 1 = 3. Store it in memo[4].
40L5n=5 left=3fib(4) = 3. Now call fib(3).
41L2n=3 depth=3Call fib(3).
42L3n=3Not a base case.
43L4n=3 memo[n]=2memo[3] is already 2. No need to recurse again.
44L4n=3 return=2Cache hit: return 2 immediately. This whole subtree is skipped.
45L5n=5 left=3 right=2 return=5fib(5) = 3 + 2 = 5. Store it in memo[5].
46L5n=6 left=5fib(5) = 5. Now call fib(4).
47L2n=4 depth=2Call fib(4).
48L3n=4Not a base case.
49L4n=4 memo[n]=3memo[4] is already 3. No need to recurse again.
50L4n=4 return=3Cache hit: return 3 immediately. This whole subtree is skipped.
51L5n=6 left=5 right=3 return=8fib(6) = 5 + 3 = 8. Store it in memo[6].
52L5return=8fib(6) = 8 with 11 calls. Plain recursion would need 25 calls.
23.05.3

House robber

A street of houses has loot value[i], and robbing two adjacent houses triggers an alarm. **State:** best[i] is the most loot from the first i houses. **Transition:** skip house i, or rob it and skip its neighbour: best[i] = max(best[i - 1], best[i - 2] + value[i]). **Base:** best[0] = 0.

On the “Classic” preset (2, 7, 9, 3, 1) the answer is 12 from houses 1, 3 and 5 (2 + 9 + 1). “One big house” (1, 2, 50, 1, 1) is 52 from houses 1, 3 and 5 too: the 50 is worth more than anything around it. The take-or-skip pattern recurs everywhere: subsets, knapsack and scheduling.

Java
1static int robO1(int[] value) {
2 int skip = 0, take = 0; // best total ending with the current house skipped / robbed
3 for (int v : value) {
4 int newTake = skip + v;
5 skip = Math.max(skip, take);
6 take = newTake;
7 }
8 return Math.max(skip, take);
9}

Best

O(n)

Average

O(n)

Worst

O(n)

Space

O(1)

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].
23.05.4

Min cost climbing stairs

Each step has a cost, paid when you step off it; you may start at step 0 or 1 and move 1 or 2 steps at a time; find the cheapest way to get past the last step. **State:** dp[i] is the cheapest cost to stand on position i (position n is the top). **Transition:** dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]). **Base:** dp[0] = dp[1] = 0, because starting is free.

The “Mixed costs” preset for 10, 15, 20, 5, 30, 8 costs 28 by standing on steps 1, 3 and 5 (15 + 5 + 8). “Skip the expensive steps” uses 1, 100, 1, 1, 1, 100, 1, 1, 100, 1 and costs 6, avoiding every 100. Notice how it differs from house robber: both choose among two predecessors, but here every path must reach the end.

Java
1static int minCostO1(int[] cost) {
2 int a = 0, b = 0; // dp[i - 2] and dp[i - 1]
3 for (int i = 2; i <= cost.length; i++) {
4 int c = Math.min(b + cost[i - 1], a + cost[i - 2]);
5 a = b;
6 b = c;
7 }
8 return b;
9}

Best

O(n)

Average

O(n)

Worst

O(n)

Space

O(1)

Min cost climbing stairs, with traceback

Interactive · edit the input

Your input

Try

cost[i]

10i=0
15i=1
20i=2
5i=3
30i=4
8i=5

dp[i] = cheapest way to stand on i

10start
15start
?start
?start
?start
?start
Setup

You can start on step 0 or step 1, so dp[0] = cost[0] and dp[1] = cost[1]. Every later step is reached from one of the two steps below it.

Step 1 / 7
Java
1static int minCost(int[] cost) { // you may start on step 0 or 1 and pay cost[i] when you leave step i
2 int n = cost.length; int[] dp = new int[n]; // dp[i] = cheapest total to stand on step i, including cost[i]
3 dp[0] = cost[0]; dp[1] = cost[1];
4 for (int i = 2; i < n; i++) dp[i] = cost[i] + Math.min(dp[i - 1], dp[i - 2]); // best predecessor, then pay for this step
5 return Math.min(dp[n - 1], dp[n - 2]); // the top is one step past the end, reached from either of the last two
6}

Live state

steps

6

No condition evaluated on this step
Execution trace (dry run table)Show 7 steps
StepLineStateWhat happens
1L3steps=6You can start on step 0 or step 1, so dp[0] = cost[0] and dp[1] = cost[1]. Every later step is reached from one of the two steps below it.
2L4i=2 dp[i-1]=15 dp[i-2]=10 dp[i]=30dp[2] = cost[2] + min(15, 10) = 20 + 10 = 30. The cheaper predecessor is step 0 (remembered for the traceback).
3L4i=3 dp[i-1]=30 dp[i-2]=15 dp[i]=20dp[3] = cost[3] + min(30, 15) = 5 + 15 = 20. The cheaper predecessor is step 1 (remembered for the traceback).
4L4i=4 dp[i-1]=20 dp[i-2]=30 dp[i]=50dp[4] = cost[4] + min(20, 30) = 30 + 20 = 50. The cheaper predecessor is step 3 (remembered for the traceback).
5L4i=5 dp[i-1]=50 dp[i-2]=20 dp[i]=28dp[5] = cost[5] + min(50, 20) = 8 + 20 = 28. The cheaper predecessor is step 3 (remembered for the traceback).
6L5top reached from=5 cost=28The top (one step past the end) is cheapest from step 5: total 28.
7L5return=28 path=[1, 3, 5]Minimum cost 28. Following the remembered predecessors backwards gives the steps stood on: [1, 3, 5].
23.05.5

Decode ways

A string of digits is decoded by letters A = 1 to Z = 26. Count the ways. **State:** dp[i] is the number of ways to decode the first i characters. **Transition:** add dp[i - 1] if the last digit is 1 to 9, and add dp[i - 2] if the last two digits form a number from 10 to 26. **Base:** dp[0] = 1.

“226” has 3 ways (2-2-6, 22-6 and 2-26), “11106” has 2, “2101” has 1 and “06” has 0. This is a **counting** DP: the transition adds instead of taking a minimum or maximum, and the same table shape then handles both.

Java
1static int numDecodingsO1(String s) {
2 int prev2 = 0, prev1 = 1; // dp[i - 2] and dp[i - 1], starting at i = 1 (dp[0] = 1)
3 for (int i = 1; i <= s.length(); i++) {
4 int cur = 0;
5 if (s.charAt(i - 1) != '0') cur += prev1;
6 if (i >= 2) {
7 int two = (s.charAt(i - 2) - '0') * 10 + (s.charAt(i - 1) - '0');
8 if (two >= 10 && two <= 26) cur += prev2;
9 }
10 prev2 = prev1;
11 prev1 = cur;
12 }
13 return prev1;
14}

Best

O(n)

Average

O(n)

Worst

O(n)

Space

O(1)

Decode ways: counting parses of a digit string

Interactive · edit the input

Your input

Try

digits

21
22
63

dp[i] = decodings of the first i digits

1i=0
?i=1
?i=2
?i=3
Setup

dp[i] counts the ways to decode the first i digits. The last digit can be a letter by itself (1-9, never 0), or the last two digits can form a letter 10-26.

Step 1 / 5
Java
1static int numDecodings(String s) { // 'A'=1 ... 'Z'=26
2 int n = s.length(); int[] dp = new int[n + 1]; // dp[i] = ways to decode the first i digits
3 dp[0] = 1; // the empty prefix has one decoding
4 for (int i = 1; i <= n; i++) {
5 if (s.charAt(i - 1) != '0') dp[i] += dp[i - 1]; // last digit alone (1-9)
6 if (i >= 2 && s.charAt(i - 2) != '0' && Integer.parseInt(s.substring(i - 2, i)) <= 26) dp[i] += dp[i - 2]; // last two digits (10-26)
7 }
8 return dp[n];
9}

Live state

digits

3

No condition evaluated on this step
Execution trace (dry run table)Show 5 steps
StepLineStateWhat happens
1L3digits=3dp[i] counts the ways to decode the first i digits. The last digit can be a letter by itself (1-9, never 0), or the last two digits can form a letter 10-26.
2L5i=1 dp[i]=1dp[1] = 1: '2' alone adds dp[0] = 1.
3L6i=2 dp[i]=2dp[2] = 2: '2' alone adds dp[1] = 1; "22" is a letter and adds dp[0] = 1.
4L6i=3 dp[i]=3dp[3] = 3: '6' alone adds dp[2] = 2; "26" is a letter and adds dp[1] = 1.
5L8return=33 ways to decode "226". O(n) time; only the last two dp values are needed, so O(1) space is possible.
23.05.6

Coin change

Fewest coins that sum to an amount, with unlimited coins. **State:** dp[a] is the fewest coins for amount a. **Transition:** dp[a] = 1 + min(dp[a - c]) over coins c <= a. **Base:** dp[0] = 0. Use a value larger than any real answer (amount + 1) for 'impossible', and return -1 at the end if dp[amount] is still that value.

On coins 1, 3, 4 and amount 6 the table ends at 2, with the coins 3 + 3, where greedy took three. “Impossible amount” (coins 5, 10, amount 7) leaves dp[7] untouched and returns -1. The same table shape counts combinations instead if you add rather than minimise, as the counting lesson later in this chapter shows.

Java
1static int coinChange(int[] coins, int amount) {
2 int[] dp = new int[amount + 1];
3 Arrays.fill(dp, amount + 1); // amount + 1 is larger than any real answer: 'impossible'
4 dp[0] = 0;
5 for (int a = 1; a <= amount; a++) {
6 for (int c : coins) {
7 if (c <= a) dp[a] = Math.min(dp[a], dp[a - c] + 1);
8 }
9 }
10 return dp[amount] > amount ? -1 : dp[amount];
11}

Best

O(amount × k)

Average

O(amount × k)

Worst

O(amount × k)

Space

O(amount)

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.

Practice ladder

Now try it yourself

Easy

  • Climbing Stairs
  • Fibonacci Number
  • Min Cost Climbing Stairs

Medium

  • House Robber
  • House Robber II
  • Decode Ways
  • Coin Change

Advanced

  • Delete and Earn
  • Decode Ways II

Key takeaways

  • Climbing stairs and Fibonacci are one recurrence with different base cases.
  • House robber is the take-or-skip pattern: best(i) = max(best(i - 1), best(i - 2) + value).
  • Minimum-cost climbing minimises over two predecessors and must reach the end.
  • Decode ways is a counting DP; coin change is an optimising DP over the same table shape.
  • Check overflow: counts grow exponentially, and Fibonacci overflows long at index 93.