Skip to main content

Updated Sep 5, 2026

Dynamic Programming

Dynamic programming applies when a problem has overlapping subproblems — the same sub-computation arising many times across a recursion. Solving each one once and reusing the answer converts exponential work into polynomial.

The name is historical and unhelpful; Richard Bellman chose it in the 1950s partly because it sounded impressive to a research sponsor. Read it as "careful recursion with a cache".

Core Concepts​

Two conditions, both required:

PropertyMeaning
Overlapping subproblemsThe same subproblem recurs. This distinguishes DP from divide and conquer
Optimal substructureAn optimal solution is built from optimal solutions to subproblems

And two ways to implement it:

Top-down (memoisation)Bottom-up (tabulation)
StructureRecursion + a cacheIteration over a table
ComputesOnly reachable subproblemsAll subproblems
OrderImplicit, from the recursionYou must choose a valid order
StackO(depth)O(depth) — can overflowNone
Easier toWrite, from the recurrenceOptimise for space

Mechanism​

The mechanical journey from a first working solution to a fast one — plain recursion, then a cache bolted on top (memoization), then the same recurrence read as a fill order over a table (tabulation), then the table collapsed to the one row it needs — is walked in full, step by step, on one worked example, in Recursion, Memoization & Tabulation. That page is the bridge from "I have a recurrence" to "I have a table"; it is not repeated here.

The workflow, on a real problem​

0/1 knapsack: choose items with weights and values, maximising value within a capacity, taking each item at most once. Greedy by value-per-weight fails here.

def knapsack(weights, values, capacity):
n = len(weights)
# dp[i][c] = best value using the first i items within capacity c
dp = [[0] * (capacity + 1) for _ in range(n + 1)]

for i in range(1, n + 1):
w, v = weights[i - 1], values[i - 1]
for c in range(capacity + 1):
dp[i][c] = dp[i - 1][c] # skip item i
if w <= c: # or take it, if it fits
dp[i][c] = max(dp[i][c], dp[i - 1][c - w] + v)
return dp[n][capacity]

Traced on items (w=1,v=1), (w=3,v=4), (w=4,v=5), (w=5,v=7) with capacity 7 — dp[i][c] is the best value using the first i items within capacity c:

i (items so far) c=0 1 2 3 4 5 6 7
0 (none) 0 0 0 0 0 0 0 0
1 (+ w1,v1) 0 1 1 1 1 1 1 1
2 (+ w3,v4) 0 1 1 4 5 5 5 5
3 (+ w4,v5) 0 1 1 4 5 6 6 9
4 (+ w5,v7) 0 1 1 4 5 7 8 9

dp[4][7] = 9 (take item 2 and item 3: weight 3+4=7, value 4+5=9)

Row i only ever reads row i - 1, which is what lets it collapse to one row — see the linked page for exactly how that collapse is derived.

WEIGHTS, VALUES, CAP = [1, 3, 4, 5], [1, 4, 5, 7], 7
assert knapsack(WEIGHTS, VALUES, CAP) == 9

Each cell depends only on the previous row, so one row suffices — provided you iterate capacity downward, so that each item is used at most once:

def knapsack_1d(weights, values, capacity):
dp = [0] * (capacity + 1)
for w, v in zip(weights, values):
for c in range(capacity, w - 1, -1): # DOWNWARD — reversing this allows reuse
dp[c] = max(dp[c], dp[c - w] + v)
return dp[capacity]
The iteration direction encodes the problem

Iterating capacity downward gives 0/1 knapsack — each item usable once. Iterating upward gives unbounded knapsack — each item usable any number of times, because dp[c - w] may already include the current item.

The two problems differ by the direction of one loop. This is the most common DP bug, and it produces a valid-looking answer to the wrong question.

Defining the state​

Most of the difficulty is choosing what the table indexes. Ask:

  1. What does one cell mean? State it as a sentence — "the best value using the first i items within capacity c". If you cannot, the state is wrong.
  2. What is the recurrence? How does a cell follow from smaller ones?
  3. What are the base cases?
  4. In what order can cells be filled so dependencies are ready?

Practical Usage​

ProblemStateComplexity
Fibonaccidp[i] = i-th numberO(n)O(n)
Climbing stairs, coin change (count ways)dp[i] = ways to reach iO(n⋅k)O(n \cdot k)
Coin change (fewest coins)dp[i] = fewest coins for amount iO(n⋅k)O(n \cdot k)
0/1 knapsackdp[i][c]O(n⋅capacity)O(n \cdot capacity)
Longest common subsequencedp[i][j] = LCS of prefixesO(n⋅m)O(n \cdot m)
Edit distance (Levenshtein)dp[i][j] = edits between prefixesO(n⋅m)O(n \cdot m)
Longest increasing subsequencedp[i] = best ending at iO(n2)O(n^2), or O(nlog⁡n)O(n \log n) with binary search
Matrix chain multiplicationdp[i][j] = best cost for the rangeO(n3)O(n^3)
# Edit distance — the engine behind spell-checkers and `diff`
def edit_distance(a, b):
m, n = len(a), len(b)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(m + 1):
dp[i][0] = i # delete all of a's prefix
for j in range(n + 1):
dp[0][j] = j # insert all of b's prefix

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] # free match
else:
dp[i][j] = 1 + min(dp[i - 1][j], # delete
dp[i][j - 1], # insert
dp[i - 1][j - 1]) # substitute
return dp[m][n]
Start top-down

Write the naive recursion first and confirm it is correct. Then add @lru_cache — often the entire optimisation. Convert to a table only if you need the space saving or hit a recursion limit. Trying to write the tabulated version directly, before the recurrence is settled, is how most DP attempts stall.

Edge Cases & Pitfalls​

  • @lru_cache requires hashable arguments. Lists must become tuples. It also holds references forever with maxsize=None, which leaks on long-running processes.
  • Mutable default arguments as caches (def f(n, memo={})) share state across calls — a real bug when the cache depends on other inputs.
  • Filling the table in the wrong order reads cells that are still zero. Bottom-up requires an order respecting every dependency — effectively a topological sort of the state graph.
  • Reconstructing the solution, not just its value, needs either a parent table or a backward walk through the finished table. Most implementations return only the optimum and then need rewriting.
  • Pseudo-polynomial complexity. Knapsack's O(n⋅capacity)O(n \cdot capacity) is polynomial in the value of the capacity but exponential in the number of bits used to write it — which is why knapsack is still NP-hard despite the DP solution.
  • Memory can be the binding constraint. An O(n⋅m)O(n \cdot m) table for two 100,000-character strings is 101010^{10} cells. Use the rolling-row trick, or Hirschberg's algorithm for linear space.

Comparisons​

DPGreedyDivide & conquerBacktracking
SubproblemsOverlapping—IndependentOverlapping or not
Choices consideredAllOneAllAll, pruned
Guarantees optimumYesOnly with a proofYesYes
Typical costPolynomialO(nlog⁡n)O(n \log n)O(nlog⁡n)O(n \log n)Exponential

Recall​

References​

  • Cormen, Leiserson, Rivest & Stein, Introduction to Algorithms, Ch. 15 — dynamic programming, with rod-cutting, matrix chains and LCS developed in full.
  • Bellman, R. (1957), Dynamic Programming — the original, and the source of the name.
  • Kleinberg & Tardos, Algorithm Design, Ch. 6 — an unusually clear treatment of choosing the state.

Books & Videos​