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
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.
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.
1static long climbStairs(int n) {2long a = 1, b = 1; // ways to reach steps i - 1 and i3for (int i = 2; i <= n; i++) { long c = a + b; a = b; b = c; }4return b;5}
Best
O(n)
Average
O(n)
Worst
O(n)
Space
O(1)
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. |
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.
1static long fibIterative(int n) {2long a = 0, b = 1; // F(i) and F(i + 1)3for (int i = 0; i < n; i++) { long c = a + b; a = b; b = c; }4return a;5}67static java.math.BigInteger fibBig(int n) {8java.math.BigInteger a = java.math.BigInteger.ZERO, b = java.math.BigInteger.ONE;9for (int i = 0; i < n; i++) { java.math.BigInteger c = a.add(b); a = b; b = c; }10return a;11}
Fibonacci with memoization
Interactive · edit the inputRecursion tree · 1 call
Call fib(6).
1static long[] memo = new long[100];2static long fib(int n) {3if (n <= 1) return n; // base cases4if (memo[n] != 0) return memo[n]; // already solved: reuse it5return memo[n] = fib(n - 1) + fib(n - 2); // solve once, remember6}
Live state
n
6
depth
1
Call stack (top first)
- fib(6)
Execution trace (dry run table)Show 52 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | n=6 depth=1 | Call fib(6). |
| 2 | L3 | n=6 | Not a base case. |
| 3 | L4 | n=6 memo[n]=0 | memo[6] is empty, so compute it. |
| 4 | L5 | n=6 | fib(6) = fib(5) + fib(4). Call fib(5) first. |
| 5 | L2 | n=5 depth=2 | Call fib(5). |
| 6 | L3 | n=5 | Not a base case. |
| 7 | L4 | n=5 memo[n]=0 | memo[5] is empty, so compute it. |
| 8 | L5 | n=5 | fib(5) = fib(4) + fib(3). Call fib(4) first. |
| 9 | L2 | n=4 depth=3 | Call fib(4). |
| 10 | L3 | n=4 | Not a base case. |
| 11 | L4 | n=4 memo[n]=0 | memo[4] is empty, so compute it. |
| 12 | L5 | n=4 | fib(4) = fib(3) + fib(2). Call fib(3) first. |
| 13 | L2 | n=3 depth=4 | Call fib(3). |
| 14 | L3 | n=3 | Not a base case. |
| 15 | L4 | n=3 memo[n]=0 | memo[3] is empty, so compute it. |
| 16 | L5 | n=3 | fib(3) = fib(2) + fib(1). Call fib(2) first. |
| 17 | L2 | n=2 depth=5 | Call fib(2). |
| 18 | L3 | n=2 | Not a base case. |
| 19 | L4 | n=2 memo[n]=0 | memo[2] is empty, so compute it. |
| 20 | L5 | n=2 | fib(2) = fib(1) + fib(0). Call fib(1) first. |
| 21 | L2 | n=1 depth=6 | Call fib(1). |
| 22 | L3 | n=1 | Base case: fib(1) = 1. |
| 23 | L3 | n=1 return=1 | return 1. |
| 24 | L5 | n=2 left=1 | fib(1) = 1. Now call fib(0). |
| 25 | L2 | n=0 depth=6 | Call fib(0). |
| 26 | L3 | n=0 | Base case: fib(0) = 0. |
| 27 | L3 | n=0 return=0 | return 0. |
| 28 | L5 | n=2 left=1 right=0 return=1 | fib(2) = 1 + 0 = 1. Store it in memo[2]. |
| 29 | L5 | n=3 left=1 | fib(2) = 1. Now call fib(1). |
| 30 | L2 | n=1 depth=5 | Call fib(1). |
| 31 | L3 | n=1 | Base case: fib(1) = 1. |
| 32 | L3 | n=1 return=1 | return 1. |
| 33 | L5 | n=3 left=1 right=1 return=2 | fib(3) = 1 + 1 = 2. Store it in memo[3]. |
| 34 | L5 | n=4 left=2 | fib(3) = 2. Now call fib(2). |
| 35 | L2 | n=2 depth=4 | Call fib(2). |
| 36 | L3 | n=2 | Not a base case. |
| 37 | L4 | n=2 memo[n]=1 | memo[2] is already 1. No need to recurse again. |
| 38 | L4 | n=2 return=1 | Cache hit: return 1 immediately. This whole subtree is skipped. |
| 39 | L5 | n=4 left=2 right=1 return=3 | fib(4) = 2 + 1 = 3. Store it in memo[4]. |
| 40 | L5 | n=5 left=3 | fib(4) = 3. Now call fib(3). |
| 41 | L2 | n=3 depth=3 | Call fib(3). |
| 42 | L3 | n=3 | Not a base case. |
| 43 | L4 | n=3 memo[n]=2 | memo[3] is already 2. No need to recurse again. |
| 44 | L4 | n=3 return=2 | Cache hit: return 2 immediately. This whole subtree is skipped. |
| 45 | L5 | n=5 left=3 right=2 return=5 | fib(5) = 3 + 2 = 5. Store it in memo[5]. |
| 46 | L5 | n=6 left=5 | fib(5) = 5. Now call fib(4). |
| 47 | L2 | n=4 depth=2 | Call fib(4). |
| 48 | L3 | n=4 | Not a base case. |
| 49 | L4 | n=4 memo[n]=3 | memo[4] is already 3. No need to recurse again. |
| 50 | L4 | n=4 return=3 | Cache hit: return 3 immediately. This whole subtree is skipped. |
| 51 | L5 | n=6 left=5 right=3 return=8 | fib(6) = 5 + 3 = 8. Store it in memo[6]. |
| 52 | L5 | return=8 | fib(6) = 8 with 11 calls. Plain recursion would need 25 calls. |
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.
1static int robO1(int[] value) {2int skip = 0, take = 0; // best total ending with the current house skipped / robbed3for (int v : value) {4int newTake = skip + v;5skip = Math.max(skip, take);6take = newTake;7}8return 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 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]. |
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.
1static int minCostO1(int[] cost) {2int a = 0, b = 0; // dp[i - 2] and dp[i - 1]3for (int i = 2; i <= cost.length; i++) {4int c = Math.min(b + cost[i - 1], a + cost[i - 2]);5a = b;6b = c;7}8return b;9}
Best
O(n)
Average
O(n)
Worst
O(n)
Space
O(1)
Min cost climbing stairs, with traceback
Interactive · edit the inputcost[i]
dp[i] = cheapest way to stand on i
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.
1static int minCost(int[] cost) { // you may start on step 0 or 1 and pay cost[i] when you leave step i2int n = cost.length; int[] dp = new int[n]; // dp[i] = cheapest total to stand on step i, including cost[i]3dp[0] = cost[0]; dp[1] = cost[1];4for (int i = 2; i < n; i++) dp[i] = cost[i] + Math.min(dp[i - 1], dp[i - 2]); // best predecessor, then pay for this step5return Math.min(dp[n - 1], dp[n - 2]); // the top is one step past the end, reached from either of the last two6}
Live state
steps
6
Execution trace (dry run table)Show 7 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | steps=6 | 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. |
| 2 | L4 | i=2 dp[i-1]=15 dp[i-2]=10 dp[i]=30 | dp[2] = cost[2] + min(15, 10) = 20 + 10 = 30. The cheaper predecessor is step 0 (remembered for the traceback). |
| 3 | L4 | i=3 dp[i-1]=30 dp[i-2]=15 dp[i]=20 | dp[3] = cost[3] + min(30, 15) = 5 + 15 = 20. The cheaper predecessor is step 1 (remembered for the traceback). |
| 4 | L4 | i=4 dp[i-1]=20 dp[i-2]=30 dp[i]=50 | dp[4] = cost[4] + min(20, 30) = 30 + 20 = 50. The cheaper predecessor is step 3 (remembered for the traceback). |
| 5 | L4 | i=5 dp[i-1]=50 dp[i-2]=20 dp[i]=28 | dp[5] = cost[5] + min(50, 20) = 8 + 20 = 28. The cheaper predecessor is step 3 (remembered for the traceback). |
| 6 | L5 | top reached from=5 cost=28 | The top (one step past the end) is cheapest from step 5: total 28. |
| 7 | L5 | return=28 path=[1, 3, 5] | Minimum cost 28. Following the remembered predecessors backwards gives the steps stood on: [1, 3, 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.
1static int numDecodingsO1(String s) {2int prev2 = 0, prev1 = 1; // dp[i - 2] and dp[i - 1], starting at i = 1 (dp[0] = 1)3for (int i = 1; i <= s.length(); i++) {4int cur = 0;5if (s.charAt(i - 1) != '0') cur += prev1;6if (i >= 2) {7int two = (s.charAt(i - 2) - '0') * 10 + (s.charAt(i - 1) - '0');8if (two >= 10 && two <= 26) cur += prev2;9}10prev2 = prev1;11prev1 = cur;12}13return 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 inputdigits
dp[i] = decodings of the first i digits
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.
1static int numDecodings(String s) { // 'A'=1 ... 'Z'=262int n = s.length(); int[] dp = new int[n + 1]; // dp[i] = ways to decode the first i digits3dp[0] = 1; // the empty prefix has one decoding4for (int i = 1; i <= n; i++) {5if (s.charAt(i - 1) != '0') dp[i] += dp[i - 1]; // last digit alone (1-9)6if (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}8return dp[n];9}
Live state
digits
3
Execution trace (dry run table)Show 5 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | digits=3 | 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. |
| 2 | L5 | i=1 dp[i]=1 | dp[1] = 1: '2' alone adds dp[0] = 1. |
| 3 | L6 | i=2 dp[i]=2 | dp[2] = 2: '2' alone adds dp[1] = 1; "22" is a letter and adds dp[0] = 1. |
| 4 | L6 | i=3 dp[i]=3 | dp[3] = 3: '6' alone adds dp[2] = 2; "26" is a letter and adds dp[1] = 1. |
| 5 | L8 | return=3 | 3 ways to decode "226". O(n) time; only the last two dp values are needed, so O(1) space is possible. |
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.
1static int coinChange(int[] coins, int amount) {2int[] dp = new int[amount + 1];3Arrays.fill(dp, amount + 1); // amount + 1 is larger than any real answer: 'impossible'4dp[0] = 0;5for (int a = 1; a <= amount; a++) {6for (int c : coins) {7if (c <= a) dp[a] = Math.min(dp[a], dp[a - c] + 1);8}9}10return 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 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. |
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.