Data Structures & Algorithms30 min readAdvanced

Dynamic Programming

Memoization, tabulation, and a framework for recognizing and solving DP problems.

What DP is — and isn't

Dynamic programming is the technique of solving a problem by breaking it into overlapping subproblems and CACHING the answer to each subproblem so you only solve it once. It applies when two conditions hold:

  • Optimal substructure — the optimal answer can be composed from optimal answers to subproblems.
  • Overlapping subproblems — the SAME subproblems appear many times in the recursion.

DP is not a single algorithm — it's a way of thinking. The hardest part is identifying the STATE: what variables uniquely describe a subproblem? Once you have that, the recurrence usually falls out.

Call stack: naive fib(4) — see the explosion
Each function call PUSHES a frame onto the stack. Returning POPS it. Recursion = stack of nested calls.
fib(4)
Call fib(4)
📓
Real-life analogy — DP = remembering your homework
Imagine your math teacher asks you the same sub-question 1000 times. Without DP, you re-solve it from scratch every time. With DP, you write the answer in a notebook the first time and just look it up after that. The 'overlapping subproblems' are the questions that keep coming back; the table is your notebook.

Two flavors: memoization vs tabulation

Longest Common Subsequence — 2D DP
Watch the DP table fill in, cell by cell. Each cell uses already-solved subproblems.
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
Strings: "ABCBDAB" vs "BDCAB". Filling DP table.
  • Memoization (top-down) — write the natural recursion, then cache results in a dict. Easy to write; uses recursion stack.
  • Tabulation (bottom-up) — fill a table from base cases up to the answer, with explicit loops. Often more memory-efficient.

Example 1: Fibonacci (the canonical bad recursion)

# Naive — O(2^n). For n=40 it already takes seconds.
def fib_slow(n):
    if n < 2: return n
    return fib_slow(n - 1) + fib_slow(n - 2)

# Top-down with @cache — O(n)
from functools import cache

@cache
def fib_memo(n):
    if n < 2: return n
    return fib_memo(n - 1) + fib_memo(n - 2)

# Bottom-up — O(n) time, O(1) space
def fib_tab(n):
    a, b = 0, 1
    for _ in range(n):
        a, b = b, a + b
    return a

print(fib_tab(50))    # instant

Example 2: Climbing stairs

You can climb 1 or 2 stairs at a time. How many ways to climb n stairs? Define `ways(n) = ways(n-1) + ways(n-2)` — and it's just Fibonacci in disguise.

Example 3: Coin change (a classic 1D DP)

Given coin denominations and an amount, find the minimum number of coins to make that amount. State: `dp[i]` = min coins to make amount i. Recurrence: try every coin.

def coin_change(coins, amount):
    INF = float("inf")
    dp = [0] + [INF] * amount
    for i in range(1, amount + 1):
        for c in coins:
            if c <= i and dp[i - c] + 1 < dp[i]:
                dp[i] = dp[i - c] + 1
    return dp[amount] if dp[amount] != INF else -1

print(coin_change([1, 2, 5], 11))   # 3 (5 + 5 + 1)

Example 4: 2D DP — longest common subsequence

Given two strings, find the length of the longest sequence of characters appearing in BOTH (not necessarily contiguous). State: `dp[i][j]` = LCS length of first i chars of A and first j chars of B.

def lcs(a, b):
    m, n = len(a), len(b)
    dp = [[0] * (n + 1) for _ in range(m + 1)]
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if a[i - 1] == b[j - 1]:
                dp[i][j] = dp[i - 1][j - 1] + 1
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
    return dp[m][n]

print(lcs("ABCBDAB", "BDCAB"))   # 4

How to spot a DP problem

  • Asking for the MINIMUM, MAXIMUM, COUNT, or BOOLEAN of something over many choices.
  • You can describe \"the answer for input X\" in terms of \"the answer for slightly smaller inputs\".
  • A naive recursion would have exponential time because of repeated subproblems.
💡 Tip
Always start with brute-force recursion. Identify the parameters that change between calls — those are your STATE. Add a cache. Then, if you want, convert to tabulation.