March 26, 20265 min read

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 algorithms interviews memoization
Ad 336x280

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:

  1. Overlapping subproblems — The same computation happens multiple times. Fibonacci calls fib(3) from both fib(5) and fib(4). Without caching, you're doing redundant work.
  1. 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.
The practical test: can you break this problem into smaller versions of the same problem? If yes, and those smaller versions overlap, it's DP.

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:

  1. Define the state. What variables describe a subproblem? For coin change, it's the remaining amount. For knapsack, it's (index, remaining capacity).
  1. Define the recurrence. How does the current state relate to smaller states? Write it as a formula.
  1. Define base cases. What are the trivial subproblems? dp[0] = 0 for coin change.
  1. Decide direction. Top-down (add memo to recursion) or bottom-up (fill a table iteratively).
  1. 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)
Once you recognize the pattern, the recurrence practically writes itself. The skill isn't mathematical brilliance — it's having seen enough problems to match patterns quickly.

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.

Ad 728x90