Expert: advanced patterns and interviews · Dynamic programming · topic 52 of 61 · 35 min
Dynamic programming: the idea
Explanation
Why we need it: the same work, again and again
In Module 5 you met the plain recursive Fibonacci function. fib(5) calls fib(4) and fib(3); fib(4) calls fib(3) again, and so on. The same calls repeat so often that the time grows like O(2ⁿ): fib(40) makes hundreds of millions of calls. Yet there are only 41 different questions (fib(0) to fib(40)). The waste is in answering the same question many times.
Dynamic programming, or DP, is the cure: answer each different question once, store the answer, and look it up the next time. The name is old and a little misleading ("programming" here meant "planning with a table"); think of it as "careful recursion with a notebook".
Two signs that DP fits
- Overlapping subproblems: the problem breaks into smaller problems of the SAME kind, and the same smaller problems come up many times. Merge sort also splits a problem, but its halves never repeat, so storing answers would not help.
- Optimal substructure: the best answer to the big problem can be built from the best answers to smaller problems. The cheapest way to reach step 10 must come through the cheapest way to reach step 9 or step 8.
Top-down and bottom-up
Top-down DP (memoisation) is the recursive function you already know, plus a dictionary or functools.cache that remembers results. You start from the big question and let the recursion ask the smaller ones. It is easy to write from a recurrence, and it only solves the subproblems you actually need.
Bottom-up DP (tabulation) fills a table, usually a list called dp, from the smallest questions upwards with a loop. There is no recursion, so no RecursionError for large n and no function-call overhead. It also makes the order of work clear, which is what lets you save space later.
| Top-down (memo) | Bottom-up (table) | |
|---|---|---|
| How | Recursion + cache | Loop filling dp[] |
| Order of work | Decided by the recursion | You choose it |
| Deep inputs | Can hit Python's recursion limit (about 1,000) | No limit problem |
| Space saving | Hard | Often easy (keep only the last rows or values) |
A four-step recipe
- State: say in words what dp[i] means. "dp[i] = the number of ways to reach step i." If you cannot say it in one sentence, you are not ready to code.
- Recurrence: how dp[i] is built from smaller states. Think about the LAST choice: the last move to step i was a 1-step from i − 1 or a 2-step from i − 2, so dp[i] = dp[i − 1] + dp[i − 2].
- Base cases: the smallest states you know without thinking. dp[0] = 1 (one way to stand still at the bottom) and dp[1] = 1.
- Order and answer: fill the table so every state you read is already filled (here i from 2 up to n), then say which cell is the answer (dp[n]).
def climb_stairs(n):
if n <= 1:
return 1
dp = [0] * (n + 1)
dp[0], dp[1] = 1, 1
for i in range(2, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]
print([climb_stairs(n) for n in range(6)]) # Output: [1, 1, 2, 3, 5, 8]Saving space: keep only what you read
dp[i] reads only dp[i − 1] and dp[i − 2], so the rest of the table is never needed again. Keep two variables and slide them forward. Time stays O(n), space drops from O(n) to O(1). Interviewers often ask for this as a follow-up.
def climb_stairs(n):
a, b = 1, 1 # ways(i - 2), ways(i - 1)
for _ in range(2, n + 1):
a, b = b, a + b
return b
print(climb_stairs(10)) # Output: 89A min-cost version
In "min cost climbing stairs", cost[i] is what you pay to step off stair i, you may start on stair 0 or stair 1, and each move goes up 1 or 2 stairs. The top is just past the last stair. State: dp[i] = the cheapest cost to stand on position i. Recurrence: dp[i] = min(dp[i − 1] + cost[i − 1], dp[i − 2] + cost[i − 2]). Base cases: dp[0] = dp[1] = 0, because you may start there for free. Answer: dp[n]. For cost = [10, 15, 20] the answer is 15: start on stair 1, pay 15 and jump 2 to the top.
Common mistakes
- Coding before you can state dp[i] in words. Fix: write the state as a comment first; most DP bugs are a fuzzy state.
- Wrong or missing base cases, for example dp[0] = 0 for counting problems. Fix: check the smallest inputs (n = 0, 1, 2) by hand against your table.
- Filling in the wrong order, so you read a cell that is still 0. Fix: make sure every cell on the right side of the recurrence is filled before the one on the left.
- Table off by one: a list of size n when you need index n. Fix: use size n + 1 when the answer is dp[n].
- Memoising with a mutable default argument or a global dict that keeps old answers between test cases. Fix: create the cache inside the function, or use functools.cache on an inner function.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Dynamic programming: the idea · 1/8
This lesson introduces dynamic programming: solving each smaller problem once, writing the answer down, and building the big answer from those.
Prefer a video by a teacher? Find free videos on “dynamic programming introduction memoization tabulation climbing stairs” on YouTube ↗
In real life: Cheapest fare across a bus route
A city bus route has stops 0 to n. A ticket covers one or two segments, and each segment has a different price at different times of the day. A journey planner wants the cheapest way to buy tickets from the first stop to the last. Trying every combination of tickets doubles with each stop, but the cheapest cost to reach stop i only depends on the cheapest costs to reach stops i − 1 and i − 2. A real planner adds passes, transfers and timetables, but its core uses the same "best answer for each smaller trip" idea.
from functools import cache
one = [12, 30, 8, 15] # price of a 1-segment ticket starting at stop i
two = [35, 25, 20] # price of a 2-segment ticket starting at stop i
@cache
def cheapest(i): # cheapest cost from stop i to the last stop (4)
if i == 4:
return 0
best = one[i] + cheapest(i + 1)
if i + 2 <= 4:
best = min(best, two[i] + cheapest(i + 2))
return best
print(cheapest(0))Practice (0/3 done)
Write climb_stairs(n) that returns the number of different ways to climb n stairs when each move is 1 or 2 stairs. n ≥ 0, and climb_stairs(0) is 1 (one way: do nothing). Use a bottom-up loop, not recursion, so n = 1000 works. Example: climb_stairs(3) returns 3 (1+1+1, 1+2, 2+1).
Write min_cost_climbing_stairs(cost). cost[i] is the price you pay to step off stair i; after paying you move up 1 or 2 stairs. You may start on stair 0 or stair 1 for free, and the top is the position just after the last stair. Return the minimum total cost to reach the top. An empty list or a one-stair list costs 0. Example: min_cost_climbing_stairs([10, 15, 20]) returns 15.
The tribonacci numbers are T0 = 0, T1 = 1, T2 = 1 and Tn = T(n − 1) + T(n − 2) + T(n − 3) for n ≥ 3. Write tribonacci(n) for n ≥ 0 using O(n) time and O(1) extra space (three variables, no list, no recursion). Example: tribonacci(4) returns 4 (0, 1, 1, 2, 4).
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- 1-D DP classicsDynamic programming · same module
- 2-D DP on grids and stringsDynamic programming · same module
- Knapsack problemsDynamic programming · same module
- Core DSA patterns in C++C++ Programming (modern C++ and DSA) · Competitive programming, interviews and careers
- RecursionC Programming (from zero to systems) · Functions and arrays
- Best, average and worst case; analysing loops and recursionThinking in Big-O
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
Why is plain recursive Fibonacci slow?
Answer: It answers the same smaller questions many times
fib(n − 2) is computed inside fib(n − 1) and again on its own, and so on all the way down: exponential repeated work.
Which problem does NOT have overlapping subproblems?
Answer: Merge sort
Merge sort splits the list into halves that never repeat, so storing answers saves nothing.
What is a main advantage of bottom-up DP over top-down in Python?
Answer: It avoids the recursion limit and makes space saving easy
A loop has no call stack to overflow, and the fixed filling order lets you keep only the last values.
In climbing stairs, why is dp[i] = dp[i − 1] + dp[i − 2]?
Answer: Because the last move into step i was a 1-step from i − 1 or a 2-step from i − 2
Every way to reach i ends with exactly one of those two moves, and the two groups do not overlap, so their counts add.
A DP has n × m states and does O(1) work per state. What is its time?
Answer: O(n × m)
Time = number of states × work per state.
Your memoized fib_memo(5000) raises RecursionError. What is the best fix?Related
Answer: Compute it bottom up with a loop
The memo removes repeated work but the recursion is still 5000 deep. A loop has no depth limit and needs only the last two values.
You only need the LCS LENGTH of two strings of lengths m and n. What is the smallest space you need with the usual DP?Related
Answer: O(min(m, n)) using two rows
Each row reads only the row above, so two rows of the shorter length are enough. To rebuild the string you need the full table (or a cleverer method).
What is the extra space of a recursive function that goes n levels deep and creates no lists?Related
Answer: O(n)
n frames wait on the call stack at the deepest point, so the space is O(n).
For house robber, what is dp[i] built from?Related
Answer: max(dp[i − 1], dp[i − 2] + nums[i − 1])
Either skip the last house (dp[i − 1]) or take it and the best from two houses back.
From 1-D DP classics
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 4 more topics in “Dynamic programming” before its exam: 1-D DP classics, 2-D DP on grids and strings, Knapsack problems, More DP patterns and how to spot them.
- Module 11 exam: Dynamic programming · 15 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, Interview strategy and mock problems. 61 topics left in the course before the final exam.