awesome-everything RU
↑ Back to the climb

Algorithms from zero

Dynamic programming: code reading

Crux Read real DP snippets, find the cache bug, the wrong state, the unsafe space optimization, and the true complexity — the diagnosis a senior makes before trusting any DP.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at middle altitude — in the sky
◷ 14 min

DP bugs are silent — they return a confidently wrong number, not a crash. Read each snippet, predict what it actually computes, and pick the fix a senior would make first.

Goal

Practise the loop you run on every DP: check the cache key matches the full state, check the transition covers every choice, check a space optimization does not destroy a value it still needs, and read the real complexity off the loops.

Snippet 1 — the memo that never hits

def lcs(a, b):
    memo = {}
    def go(i, j):
        if i == len(a) or j == len(b):
            return 0
        if a in memo:                 # bug: keying on the wrong thing
            return memo[a]
        if a[i] == b[j]:
            res = 1 + go(i + 1, j + 1)
        else:
            res = max(go(i + 1, j), go(i, j + 1))
        memo[a] = res
        return res
    return go(0, 0)
Quiz

This memoized LCS is both wrong and slow. What is the defect and the fix?

Snippet 2 — the coin-change state

def min_coins(coins, amount):
    # one coin per denomination, no reuse
    dp = [0] + [float('inf')] * amount
    for c in coins:
        for x in range(amount, c - 1, -1):   # capacity downward
            dp[x] = min(dp[x], dp[x - c] + 1)
    return dp[amount] if dp[amount] != float('inf') else -1
Quiz

The author wants UNLIMITED use of each coin (classic coin change). Does this 1D code do that, and what does the iteration direction control?

Snippet 3 — the rolling-row optimization

def edit_distance(a, b):
    n, m = len(a), len(b)
    prev = list(range(m + 1))          # row for i = 0
    for i in range(1, n + 1):
        cur = [i] + [0] * m            # cur[0] = i
        for j in range(1, m + 1):
            if a[i - 1] == b[j - 1]:
                cur[j] = prev[j - 1]
            else:
                cur[j] = 1 + min(prev[j], cur[j - 1], prev[j - 1])
        prev = cur
    return prev[m]
Quiz

This edit-distance code rolls the 2D table down to two rows. Is the space optimization correct, and what is the dependency that makes it safe?

Snippet 4 — the interval-DP loops

def interval_cost(n, weight):
    dp = [[0] * n for _ in range(n)]
    for length in range(2, n + 1):                # interval length
        for i in range(0, n - length + 1):        # left endpoint
            j = i + length - 1                    # right endpoint
            best = float('inf')
            for k in range(i, j):                 # split point
                best = min(best, dp[i][k] + dp[k + 1][j] + weight(i, k, j))
            dp[i][j] = best
    return dp[0][n - 1]
Quiz

What is the time and space complexity of this interval DP, and where does the dominant cost come from?

Recap

Four checks catch most DP defects: the cache key must equal the full subproblem state (Snippet 1 keyed on the wrong variable); the transition and loop direction must match the intended semantics (Snippet 2 — downward is 0/1, upward is unbounded); a rolling-row space cut is safe only when each cell reads just the previous row and the current row’s neighbors (Snippet 3); and the real complexity is the product of the loop bounds — O(n^2) states times O(n) work per state is O(n^3) (Snippet 4). Read the loops, do not trust the comment.

Continue the climb ↑Dynamic programming: build and prove a DP solver
shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.