Algorithmic Patterns
Dynamic programming
Overlapping subproblems and optimal substructure
Some problems break into smaller copies of themselves - and those copies keep reappearing. Naive recursion re-solves each one from scratch, and the repeated work explodes exponentially. Dynamic programming (DP) fixes this with one embarrassingly simple idea: solve each distinct subproblem exactly once, store its answer, and look it up ever after. Trading a little memory for a lot of recomputation collapses exponential algorithms into polynomial ones. We will watch it happen on edit distance - the algorithm that measures how many single-character edits separate two words.
The one-line idea
Two conditions that make DP work
DP is not magic and it does not apply everywhere. A problem is a DP problem when it has two properties at once:
1. Overlapping subproblems. The naive recursion asks the same question many times. For edit distance, edit(i, j) - the cost to align the first letters of one word with the first of another - branches into edit(i-1, j), edit(i, j-1), and edit(i-1, j-1). Those branches fan out exponentially, yet they collide constantly: there are only distinct pairs. Everything past that first visit is wasted work.
2. Optimal substructure. An optimal answer to the whole problem is built from optimal answers to its subproblems. If you know the best way to edit the shorter prefixes, one more comparison extends it to the best way to edit the longer ones. That is exactly the "take the minimum of three options" step you are about to see - and it is what makes storing subresults safe.
Without overlap, DP buys you nothing
State and transition: the edit-distance recurrence
Every DP is two decisions. First, define the state - the smallest set of facts that names a subproblem. For edit distance between word (length ) and word (length ), the state is a pair , and
Second, define the transition - how a state's answer is assembled from smaller states. To match one more letter you either delete from (step down a row), insert into (step right a column), or substitute along the diagonal - free if the two letters already match. Take the cheapest:
The base cases anchor the recursion. Turning any prefix into the empty string is all deletions; building any prefix from is all insertions:
The indicator is when the letters differ (a real substitution) and when they already match (the diagonal is free). Fill the grid so that every cell's three neighbours are ready before it, and the bottom-right corner is the answer.
Fill the table, then trace back
Here is the recurrence made physical. We turn cat into cute. Step through the interior cell by cell: each cell reaches up (delete), left (insert), and diagonally (substitute/match), takes the smallest, and records it. When the grid is full, the corner holds the edit distance - and following each cell back to the neighbour it chose reconstructs the exact edits.
Base cases first. The top row and left column are free - turning a prefix into ε is all deletes; building one from ε is all inserts.
The rows are the letters of cat (plus the empty string ε on top); the columns are the letters of cute (plus ε on the left). Each interior cell is the min of its three filled neighbours.
The aha: the table is the recursion, memoized
edit(1, 1) - the "match c" cell - from scratch for nearly every path through the tree. Here it is computed a single time and read four times during traceback. Same answer, but recomputation becomes lookups. The traceback is the payoff: the numbers alone tell you the distance is 2, but the arrows tell you which two edits - substitute a→u, then insert e. That reconstruction is what turns a cost into an actual answer.Top-down vs bottom-up: two ways to fill the same table
There are exactly two ways to populate the DP table, and they compute identical values.
Top-down (memoization) keeps the natural recursion but adds a cache: before solving a state, check whether it is already stored; after solving, store it. You only ever touch the states you actually need, and the code reads like the recurrence.
Bottom-up (tabulation) throws away recursion entirely and fills the grid in dependency order with plain loops - smallest subproblems first - so every neighbour is ready when you reach a cell. No call stack, slightly faster in practice, and easy to shrink to memory by keeping just one row. That is the version the demo animates:
def edit_distance(a, b): # Levenshtein: fewest insert/delete/substitute
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 every letter of a
for j in range(n + 1): dp[0][j] = j # insert every letter of b
for i in range(1, m + 1):
for j in range(1, n + 1):
cost = 0 if a[i - 1] == b[j - 1] else 1
dp[i][j] = min(dp[i - 1][j] + 1, # delete
dp[i][j - 1] + 1, # insert
dp[i - 1][j - 1] + cost) # substitute / match
return dp[m][n]
edit_distance("cat", "cute") # -> 2Why it is fast, and where the memory goes
Check yourself
Dynamic programming pays off precisely when a problem's subproblems are:
Each interior cell of the edit-distance table is filled by taking the minimum over:
Recall the edit-distance recurrence, its base cases, and its running time.
Recall the loop invariant. Try to state it, then check.
Lock it in
- DP applies exactly when a problem has both overlapping subproblems and optimal substructure: solve each distinct subproblem once, store it, and look it up ever after.
- Trading a little memory for a lot of recomputation collapses exponential recursion into polynomial time.
- Edit distance fills an table; each interior cell is the min of its up, left, and diagonal neighbours plus a step, so the whole thing is time and space.
- A rolling row cuts space to , but you forfeit the traceback that reconstructs the actual alignment.
Primary source
Ask your teacher