Dynamic Programming

Dynamic programming solves problems by breaking them into overlapping subproblems and storing solutions to avoid recomputation. It applies when a problem has optimal substructure and overlapping subproblems.

Recognising DP problems

A problem is likely DP if:

  • It asks for a count, maximum, minimum, or boolean feasibility
  • It involves making choices at each step (include/exclude, left/right)
  • A recursive brute-force solution has exponential time due to repeated subproblems

If you can write a brute-force recursion, you’re 80% of the way to a DP solution. Add memoization and you have top-down DP. Flip the recursion to iteration and you have bottom-up DP.

Top-Down (Memoization)

Write the recursion naturally. Cache results in a dictionary.

# Fibonacci — O(n) with memoization vs O(2^n) without
from functools import cache

@cache
def fib(n: int) -> int:
    if n <= 1:
        return n
    return fib(n - 1) + fib(n - 2)

Bottom-Up (Tabulation)

Build up the solution iteratively from base cases.

def fib(n: int) -> int:
    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]

# Space-optimised: O(1) space
def fib_opt(n: int) -> int:
    a, b = 0, 1
    for _ in range(n):
        a, b = b, a + b
    return a

Classic patterns

0/1 Knapsack

Choose items to maximise value without exceeding weight capacity. Each item can be taken at most once.

def knapsack(weights: list[int], values: list[int], capacity: int) -> int:
    n = len(weights)
    dp = [[0] * (capacity + 1) for _ in range(n + 1)]
    for i in range(1, n + 1):
        for w in range(capacity + 1):
            dp[i][w] = dp[i-1][w]
            if weights[i-1] <= w:
                dp[i][w] = max(dp[i][w], dp[i-1][w - weights[i-1]] + values[i-1])
    return dp[n][capacity]

Longest Common Subsequence (LCS)

A string DP template used in diff tools, plagiarism detection, and DNA alignment.

def lcs(s1: str, s2: str) -> int:
    m, n = len(s1), len(s2)
    dp = [[0] * (n + 1) for _ in range(m + 1)]
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if s1[i-1] == s2[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]

Complexity

ProblemTimeSpace
FibonacciO(n)O(n) → O(1) optimised
KnapsackO(n × W)O(n × W) → O(W) optimised
LCSO(m × n)O(m × n) → O(n) optimised
Coin changeO(amount × coins)O(amount)
Edit distanceO(m × n)O(m × n)

Space optimisation in 2D DP: if each row depends only on the previous row, you can reduce from O(m × n) to O(n) by keeping two rows (or one, with careful ordering).