Skip to content

Expert: advanced patterns and interviews · Dynamic programming · topic 52 of 61 · 35 min

Dynamic programming: the idea

Explanation

Dynamic programming (DP) solves a big problem by solving smaller versions of it once each and storing the answers. It works when the same small problems come up again and again (overlapping subproblems) and the best answer is built from the best answers to smaller problems (optimal substructure). You can fill the answers from the top with memoisation, or from the bottom with a table, and often shrink the table to two variables.

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)
HowRecursion + cacheLoop filling dp[]
Order of workDecided by the recursionYou choose it
Deep inputsCan hit Python's recursion limit (about 1,000)No limit problem
Space savingHardOften easy (keep only the last rows or values)

A four-step recipe

  1. 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.
  2. 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].
  3. Base cases: the smallest states you know without thinking. dp[0] = 1 (one way to stand still at the bottom) and dp[1] = 1.
  4. 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]).
python
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]
O(n) time and O(n) space. The number of ways follows the Fibonacci numbers.

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.

python
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: 89

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

Tip: How to get the complexity of any DP: (number of states) × (work per state). Climbing stairs has n states with O(1) work each, so O(n). A table with n × m cells and O(1) work per cell is O(n × m).

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

Dynamic programming

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.

python
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))
Shows 52: a 1-segment ticket from stop 0 (12), a 2-segment ticket from stop 1 to 3 (25), then a 1-segment ticket to stop 4 (15). Each stop is solved once.

Practice (0/3 done)

Task 1 · Code · Python

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

Task 2 · Code · Python

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.

Task 3 · Code · Python

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

Question 1

1.Why is plain recursive Fibonacci slow?

Question 2

2.Which problem does NOT have overlapping subproblems?

Question 3

3.What is a main advantage of bottom-up DP over top-down in Python?

Question 4

4.In climbing stairs, why is dp[i] = dp[i − 1] + dp[i − 2]?

Question 5

5.A DP has n × m states and does O(1) work per state. What is its time?

Read more (free)

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.

From Memoization: remembering answers

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

From 2-D DP on grids and strings

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

From Recursion: functions that call themselves

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

  1. 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.
  2. Module 11 exam: Dynamic programming · 15 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Interview strategy and mock problems. 61 topics left in the course before the final exam.

← Previous: Bit tricks in interviews