Dynamic Programming¶
Difficulty: Medium–Hard | Pattern Type: Overlapping subproblems
Why This Pattern Exists¶
Problem: A staircase has n steps. You can climb 1 or 2 steps at a time. How many distinct ways can you reach the top?
Brute force: from step i, try i+1 and i+2. Recurse.
ways(5) → ways(4)+ways(3)
ways(4) → ways(3)+ways(2) ← ways(3) computed twice
ways(3) → ways(2)+ways(1) ← ways(2) computed many times
For n=40 this is tens of millions of calls. The call tree is exponential, but there are only n different questions: ways(k) for k = 0..n.
Clue: the same subproblem shows up more than once (overlapping subproblems), and the answer to n is built only from answers to smaller n (optimal substructure). That pair is DP. Not “a 2D array.” The array is an implementation detail.
Mental Model¶
You are filling a table of answers to smaller questions, in an order that guarantees those answers already exist.
Question: ways(i) = ways(i-1) + ways(i-2)
Base: ways(0)=1, ways(1)=1
i: 0 1 2 3 4 5
ways: 1 1 2 3 5 8
↑
just looked left
If you cannot name the question a cell answers in one sentence, you do not have a recurrence yet. Stop coding.
From brute force to optimal — Fibonacci / climb stairs¶
Observe: memoization and the table compute the same recurrence. Top-down is easier when the state space is sparse; bottom-up is easier to reason about complexity and to shrink space.
Pattern recognition¶
| You hear… | State is probably… |
|---|---|
| “number of ways” | dp[i] = sum of ways to reach i |
| “minimum / maximum cost” | dp[i] = min/max over choices |
| “can you reach / is it possible” | boolean DP or BFS |
| “include or skip this item” | 0/1 knapsack, dp[i][w] |
| “unbounded supply of coins/items” | coin change, inner loop over amounts |
| “longest increasing / common” | dp[i] ends at index i |
| grid, “only right/down” | dp[r][c] from top/left |
Interview Insight 🎯
Say the state out loud: “dp[i][j] is the minimum coins to make amount j using the first i coin types.” If the interviewer nods, write the loop. If they frown, the state is wrong — not the syntax.
0/1 Knapsack — include or skip¶
Problem: n items, weights w[i], values v[i], capacity W. Each item at most once. Max value.
Brute force: 2^n subsets.
Structure: after deciding item i, you have a smaller prefix and remaining capacity. Overlap: many prefixes share the same remaining W.
dp[i][c] = max value using items[0..i) with capacity c
dp[i][c] = dp[i-1][c] # skip
= max(that, v[i-1] + dp[i-1][c-w[i-1]]) # take, if w fits
Worked example — items (w=1,v=1), (w=3,v=4), (w=4,v=5), capacity=5. Each row only reads the row above it:
c: 0 1 2 3 4 5
i=0 (none) 0 0 0 0 0 0
i=1 (w1,v1) 0 1 1 1 1 1
i=2 (w3,v4) 0 1 1 4 5 5
i=3 (w4,v5) 0 1 1 4 5 6
↑
dp[3][5]=6: skip item2 (w3,v4), take item1+item3 (w1,v1)+(w4,v5)=6
dp[i][c] copies straight up from dp[i-1][c] when skipping, or reaches up and left by w[i-1] when taking — exactly the two terms in the recurrence above.
def knapsack(w: list[int], v: list[int], cap: int) -> int:
n = len(w)
dp = [[0] * (cap + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
for c in range(cap + 1):
dp[i][c] = dp[i - 1][c]
if w[i - 1] <= c:
take = v[i - 1] + dp[i - 1][c - w[i - 1]]
if take > dp[i][c]:
dp[i][c] = take
return dp[n][cap] # O(n·cap) time and space
Space drop: one row of size cap+1, iterate c downward so c - w is still the previous item.
def knapsack_1d(w, v, cap):
dp = [0] * (cap + 1)
for wi, vi in zip(w, v):
for c in range(cap, wi - 1, -1):
dp[c] = max(dp[c], vi + dp[c - wi])
return dp[cap]
func knapsack(w, v []int, cap int) int {
dp := make([]int, cap+1)
for i, wi := range w {
for c := cap; c >= wi; c-- {
if t := v[i] + dp[c-wi]; t > dp[c] {
dp[c] = t
}
}
}
return dp[cap]
}
Production Trap ⚠️
O(n·W) is pseudo-polynomial. W = 10^9 is not “a loop.” If capacity is huge and n is tiny, meet-in-the-middle or branch-and-bound — not a 10^9 array.
Coin change — unbounded knapsack¶
Problem: coins C, amount A. Fewest coins to make A, unlimited of each. Impossible → -1.
Brute force: recursion on remaining amount — same remainder asked over and over.
Recurrence: dp[x] = min(dp[x], dp[x - coin] + 1) for each coin that fits.
Order: for unbounded, loop coins outer, amount inner ascending — you may reuse the same coin.
Interactive walkthrough¶
Coins [1, 3, 4], amount 6. Each cell is dp[amount]. ∞ = not yet makeable. Step until dp[6].
Legend: index under cell = amount. Green = finite value. Orange = just relaxed. Blue = dp[0].
The visualizer may nest amount-outer (for each amount, try every coin). The unbounded-knapsack / coin-change code on this page is coins-outer, amount-inner. Loop order is what distinguishes 0/1 (capacity inner, descending so each item is used at most once) from unbounded (amount inner, ascending so a coin can be reused).
def coin_change(coins: list[int], amount: int) -> int:
inf = amount + 1
dp = [inf] * (amount + 1)
dp[0] = 0
for coin in coins:
for x in range(coin, amount + 1):
dp[x] = min(dp[x], dp[x - coin] + 1)
return dp[amount] if dp[amount] < inf else -1
# O(len(coins) · amount) time, O(amount) space
func coinChange(coins []int, amount int) int {
inf := amount + 1
dp := make([]int, amount+1)
for i := range dp {
dp[i] = inf
}
dp[0] = 0
for _, c := range coins {
for x := c; x <= amount; x++ {
if dp[x-c]+1 < dp[x] {
dp[x] = dp[x-c] + 1
}
}
}
if dp[amount] >= inf {
return -1
}
return dp[amount]
}
Variant — number of combinations (order does not matter): same loop nest, dp[0]=1, dp[x] += dp[x-coin]. Swap loops (amount outer) and you count permutations instead — a classic interview trap.
Unique paths — grid DP¶
Problem: m × n grid, start top-left, finish bottom-right, only right or down. How many paths?
Brute force: DFS, C(m+n-2, m-1) leaves — but overlapping prefixes.
def unique_paths(m: int, n: int) -> int:
dp = [1] * n
for _ in range(1, m):
for c in range(1, n):
dp[c] += dp[c - 1]
return dp[-1] # O(m·n) time, O(n) space
Obstacles: set dp[r][c] = 0 on a blocked cell; first row/col stop propagating after a block.
This is the same shape as climb-stairs in 2D. Once you have seen both, you should smell DP on any “count ways on a DAG of states.”
LIS — dp[i] means “best ending here”¶
Problem: longest increasing subsequence (not subarray — not sliding window).
def lis_length(a: list[int]) -> int:
n = len(a)
dp = [1] * n # LIS ending at i
for i in range(n):
for j in range(i):
if a[j] < a[i]:
dp[i] = max(dp[i], dp[j] + 1)
return max(dp) if dp else 0
# O(n²). O(n log n): patience sorting / tails binary search
Clue: “subsequence” + “longest” + no contiguous requirement → usually dp[i] or patience, not sliding window.
Edge cases¶
# Coin change
coin_change([2], 3) # -1, not a crash on ∞
coin_change([1], 0) # 0
coin_change([1, 2, 5], 11) # 3
# Knapsack
knapsack_1d([], [], 10) # 0
knapsack_1d([5], [10], 3) # 0, item does not fit
# Paths
unique_paths(1, 1) # 1
# Obstacles on start or end → 0
Integer overflow: Go int is fine for typical LC; “number of ways” problems often need mod 1e9+7. Python ints are unbounded — still apply the modulus when the prompt says so.
Complexity cheat sheet¶
| Problem | Time | Space after squeeze |
|---|---|---|
| Climb stairs / Fibonacci | O(n) | O(1) |
| 0/1 knapsack | O(nW) | O(W) |
| Coin change (min coins) | O(nA) | O(A) |
| Unique paths | O(mn) | O(n) |
| LIS naive | O(n²) | O(n) |
Interview follow-ups¶
- “Reconstruct the coins, not just the count.” Keep
prev[x] = cointhat last improveddp[x]; walk back fromA. - “What if coins can be negative?” Recurrence can cycle. DP on amounts is the wrong model.
- “Top-down vs bottom-up?” Sparse / early exit → memo. Tight loops, space squeeze, interview default → bottom-up.
- “Is this greedy?” Coin change is greedy only for canonical coin systems.
[1,3,4]amount6is3+3, not4+1+1. If you cannot prove the greedy choice property, use DP.
Key takeaways¶
Remember
- Two properties: overlapping subproblems + optimal substructure. Missing either → not DP (or not this DP).
- Write the English state, then the recurrence, then the loop order (0/1 vs unbounded is the inner-loop direction).
- Brute force → memo → table → roll the array. Do not start at step 4 in an interview.
- “Contiguous” is sliding window; “subsequence / ways / min cost with reuse” is DP.
- Name the cell: if you cannot, you are guessing indices.