Dynamic Programming Isn't Magic — Here's How to Actually Learn It
DP is pattern recognition, not genius. Top-down vs bottom-up, how to spot DP problems, and worked examples from Fibonacci to coin change.
Dynamic programming has this reputation as the hardest topic in coding interviews. It's not. It's just poorly taught. Most explanations jump straight to a state transition formula and expect you to nod along. The actual skill is recognizing when a problem has DP structure — and that's pure pattern recognition. You can train it.
Why DP Feels Hard
Here's the thing people don't say: DP is just recursion with caching. That's it. If you can write a recursive brute-force solution, you're 80% of the way to a DP solution. The remaining 20% is figuring out what to cache.
The reason it feels hard is that problems are usually presented in their optimized bottom-up form, which obscures the thinking process that got there. Nobody starts with the tabulation table. You start with recursion, notice repeated work, and then optimize.
The Two Approaches
Top-Down (Memoization)
Write the recursive solution first. Then add a cache. Done.
# Fibonacci — recursive (slow, exponential)
def fib(n):
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)
# Fibonacci — top-down DP (fast, O(n))
def fib_memo(n, memo={}):
if n <= 1:
return n
if n in memo:
return memo[n]
memo[n] = fib_memo(n - 1, memo) + fib_memo(n - 2, memo)
return memo[n]
Same logic, same structure. The only addition is the memo dictionary. Each subproblem is solved once and cached.
Bottom-Up (Tabulation)
Instead of recursion, build the solution iteratively from the smallest subproblems up.
# Fibonacci — bottom-up DP
def fib_tab(n):
if n <= 1:
return n
dp = [0] * (n + 1)
dp[1] = 1
for i in range(2, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]
No recursion, no stack overflow risk, and often slightly faster in practice due to no function call overhead. But conceptually, it's doing the exact same thing.
My recommendation: always start top-down. Get the recursive structure right, add memoization, verify it works. Convert to bottom-up later if you need to, but in an interview, top-down with memoization is perfectly fine.How to Spot a DP Problem
Two conditions, both must be true:
- Overlapping subproblems — The same computation happens multiple times. Fibonacci calls
fib(3)from bothfib(5)andfib(4). Without caching, you're doing redundant work.
- Optimal substructure — The optimal solution to the whole problem is built from optimal solutions to subproblems. The shortest path from A to C through B uses the shortest path from A to B.
Worked Example: Climbing Stairs
You can climb 1 or 2 steps at a time. How many distinct ways to reach step n?
Think recursively. From step n, you could have come from step n-1 (took 1 step) or step n-2 (took 2 steps). So:
ways(n) = ways(n - 1) + ways(n - 2)
That's literally Fibonacci. Base cases: ways(0) = 1 (one way to stay put), ways(1) = 1.
def climb_stairs(n):
if n <= 1:
return 1
dp = [0] * (n + 1)
dp[0] = 1
dp[1] = 1
for i in range(2, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]
You can even optimize space to O(1) since you only need the last two values:
def climb_stairs(n):
a, b = 1, 1
for _ in range(2, n + 1):
a, b = b, a + b
return b
Worked Example: Coin Change
Given coins of different denominations and a target amount, find the minimum number of coins needed.
This one's a bit meatier. For each amount from 1 to target, try every coin. If using that coin leaves a valid remaining amount, consider it.
def coin_change(coins, amount):
dp = [float('inf')] * (amount + 1)
dp[0] = 0 # 0 coins to make amount 0
for a in range(1, amount + 1):
for coin in coins:
if coin <= a and dp[a - coin] != float('inf'):
dp[a] = min(dp[a], dp[a - coin] + 1)
return dp[amount] if dp[amount] != float('inf') else -1
The recurrence: dp[amount] = min(dp[amount - coin] + 1) for each valid coin. Each subproblem (dp[a - coin]) is a smaller version of the original problem. Classic DP.
The DP Framework
When you see a new problem, run through this:
- Define the state. What variables describe a subproblem? For coin change, it's the remaining amount. For knapsack, it's (index, remaining capacity).
- Define the recurrence. How does the current state relate to smaller states? Write it as a formula.
- Define base cases. What are the trivial subproblems?
dp[0] = 0for coin change.
- Decide direction. Top-down (add memo to recursion) or bottom-up (fill a table iteratively).
- Optimize space if possible. Many 1D DP problems only look back a fixed number of steps.
Common DP Patterns to Recognize
- Linear DP — state depends on previous 1-2 elements (Fibonacci, climbing stairs, house robber)
- Knapsack — choose items with weight/value constraints (subset sum, partition equal subset)
- String DP — two strings, 2D table (edit distance, longest common subsequence, regex matching)
- Interval DP — subproblems defined by ranges (matrix chain multiplication, burst balloons)
- Grid DP — paths in a 2D grid (unique paths, minimum path sum)
If you want to build that pattern recognition through practice, CodeUp has a curated set of DP problems organized by pattern, with step-by-step hints that guide you toward the recurrence instead of just showing you the answer.