Dynamic programming (DP) solves a problem by combining the answers to smaller versions of it, and storing each answer so it is computed only once. It applies when the problem has
- optimal substructure: the best answer is built from the best answers to sub-problems, and
- overlapping sub-problems: the same sub-problems come up again and again.
Hints in the wording: "the number of ways", "the minimum cost", "the maximum value", "is it possible", over a sequence or grid. (The Python course's memoization lesson and the Algorithms course's DP lessons go further.)
The recipe
- State: what does dp[i] mean, in words? ("the most money robbing houses 0 to i")
- Recurrence: how does dp[i] come from smaller states?
- Base cases: the smallest states, known directly.
- Order: compute states so that each one's ingredients are ready (bottom-up), or recurse with a cache (top-down).
- Answer: which state is it?
House robber
Houses in a row hold money; robbing two neighbours sets off the alarm. What is the most you can take?
State: best[i] = the most from houses 0..i. For house i, either skip it (best[i − 1]) or rob it (money[i] + best[i − 2]):
best[i] = max(best[i − 1], money[i] + best[i − 2])
def rob(money):
before_previous, previous = 0, 0 # best[i - 2] and best[i - 1]
for m in money:
current = max(previous, m + before_previous) # skip this house, or rob it
before_previous, previous = previous, current
return previous
print(rob([2, 7, 9, 3, 1]))
print(rob([5, 1, 1, 5]))Each state needs only the two before it, so two variables replace the table: O(n) time and O(1) space.
Coin change
The fewest coins that make an amount, or −1 if impossible.
State: fewest[a] = the fewest coins for amount a. The last coin is some c, so fewest[a] = 1 + min(fewest[a − c]) over the coins c ≤ a. Base case: fewest[0] = 0. Amounts that cannot be made stay at infinity.
def coin_change(coins, amount):
INF = float("inf")
fewest = [0] + [INF] * amount # fewest[a]: coins needed for amount a
for a in range(1, amount + 1):
for c in coins:
if c <= a and fewest[a - c] + 1 < fewest[a]:
fewest[a] = fewest[a - c] + 1
return fewest[amount] if fewest[amount] != INF else -1
print(coin_change([1, 3, 4], 6))
print(coin_change([2], 3))O(amount × coins). The greedy "always take the biggest coin" fails for coins like [1, 3, 4] and amount 6: greedy takes 4 + 1 + 1 (3 coins), but 3 + 3 is 2. DP tries every last coin, so it cannot be fooled.
More patterns
| Problem | State | Recurrence |
|---|---|---|
| climbing stairs (1 or 2 steps) | ways[i] to reach step i | ways[i − 1] + ways[i − 2] |
| longest increasing subsequence | lis[i] ending at i | 1 + max(lis[j]) for j < i with a[j] < a[i] |
| unique grid paths | paths[r][c] | paths[r − 1][c] + paths[r][c − 1] |
| 0/1 knapsack | best[i][w] with i items, capacity w | max(skip, take) |
| edit distance | d[i][j] for prefixes | 1 + min of three neighbours, or d[i−1][j−1] if equal |
In an interview, say the state and recurrence before coding. If the recurrence is right, the code is easy; if it is wrong, no amount of code will help.
Quick check
Practice problems for this lesson
My notes
Write down what you want to remember from this lesson. Notes are saved on this device as you type, and appear on your dashboard.