Algorithms from zero
Dynamic programming: code reading
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.
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)
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
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]
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]
What is the time and space complexity of this interval DP, and where does the dominant cost come from?
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.