Backtracking and dynamic programming
Before this lesson: lesson 02 (binary search, greedy choices) and lesson 03 (DFS, DAGs, topological order, shortest paths). Time: about 80 minutes, plus the notebook.
Why this matters #
DP rounds test one skill: say the state and the recurrence before you code. Most lost time comes from base cases, loop order and off-by-one prefix indices, so this lesson gives a fixed script and a table of the common state shapes. Backtracking rounds test whether you can write the template without bugs (copy the solution, undo the choice) and bound its cost.
The same tables run inside ML. The word error rate (WER) of a speech model is an edit distance. The CTC loss of speech recognition and the forward and Viterbi algorithms of hidden Markov models are DP over (time step, state). Dynamic time warping aligns time series with an edit-distance-like table. The Bellman equation of reinforcement learning is a DP recurrence. Beam search in language model decoding is a pruned search over a tree of partial outputs.
Notation. is the input size (number of items or length of a string); and are the lengths of two strings; is an amount or a budget.
1. Predict first #
2. The picture #
Backtracking walks a tree of partial solutions #
Each node of the tree is a partial solution, each edge is one choice, and each leaf is a complete candidate. The simplest case is the subsets of : at depth you skip or take item .
[]
skip 1 / \ take 1
[] [1]
skip 2 / \ / \ take 2
[] [2] [1] [1, 2] 4 leaves = 2^2 subsets
The recursion is a depth-first search of this tree (lesson 03).
One list cur holds the current path. You choose (append), explore (recurse), then
undo (pop), so that cur is correct for the next choice. Pruning cuts a subtree as soon as
its partial solution cannot be completed. For the 8-queens problem (place 8 queens on a chessboard
so that no two attack each other), placing one queen per row and skipping attacked squares visits
2,057 nodes, against complete boards without pruning.
Step through the search. On the board, the blue queens are the current
partial solution and the magenta squares are attacked. In the tree, each node
is a partial solution (the edge label is the column of the new queen); the path from the root to
the blue node is cur,
aqua leaves are solutions, and the counters show how many squares were tried
and pruned. Switch to "subsets of {1, 2, 3}" to see the plain template without pruning: 15 nodes,
8 leaves.
Dynamic programming merges equal nodes #
The recursion tree of fib(5), written f(5):
f(5)
/ \
f(4) f(3) f(3) appears 2 times,
/ \ / \ f(2) 3 times, f(1) 5 times:
f(3) f(2) f(2) f(1) 15 calls for 6 different values
/ \ / \ / \
f(2) f(1) f(1) f(0) f(1) f(0)
/ \
f(1) f(0)
The value of f(3) does not depend on the path that led to it, so you can compute it once and reuse
it. If you merge equal nodes, 6 states remain, and each with points to
and : a small DAG of states (directed acyclic graph, lesson 03).
Move the slider and switch the view. Node is the call , and nodes with the same argument have the same colour. "With a memo" shows the calls that really run: repeated calls return from the cache (dashed). The tree has calls ( = Fibonacci numbers), which grows like ; the DAG has only states and edges. A memo turns the tree into the DAG.
So ask: does the answer below a node depend only on a small description of where you are (the state), or on the whole path? A small state that repeats calls for DP. If you must output every path, or the state is the whole path, use backtracking.
How many calls (including the first one) does the plain recursive fib(4) make?
with , so , , . In general : with , . Five is the number of different states , which is what a memo computes. Sixteen () is a rough upper bound that ignores that the branch is shorter than .
This subsets code returns [[], [], [], [], [], [], [], []] for [1, 2, 3]. Where is the bug?
def subsets(nums):
out, cur = [], []
def go(i):
if i == len(nums):
out.append(cur)
return
go(i + 1) # skip nums[i]
cur.append(nums[i]); go(i + 1); cur.pop() # take nums[i]
go(0)
return out
Every leaf appends a reference to the one list cur. At the end all choices are undone, so cur is empty, and all 8 entries show the same empty list. Record a copy: out.append(cur[:]). The order of the two branches only changes the order of the output; popping before exploring would lose the choice.
3. Definitions and main results #
Backtracking #
go(state):
if state is complete: record a COPY of it; return
for each choice allowed in state:
if the choice can still lead to a solution: # pruning
apply the choice; go(new state); undo the choice
Three shapes cover most problems. They differ in how they avoid listing the same solution twice:
| Problem | Choice at each depth | No duplicates because | Leaves | Time |
|---|---|---|---|---|
| subsets | skip or take the next item | items are decided in a fixed order | ||
| combinations of size | the next item, with index larger than the last one | a start index | if you stop when too few items are left | |
| permutations | any unused item | a used array |
The cost of backtracking is (number of nodes) × (work per node), plus the cost of copying each solution. The next lemma shows that, for permutations, the inner nodes cost only a constant factor more than the leaves.
Lemma (size of the permutation tree). The tree of all permutations of items has nodes.
Proof.
- A node at depth is an ordered choice of different items out of . There are of them.
- Sum over and write : , because .
Each node scans candidates and each leaf copies items, so the total is .
Pruning comes in three kinds: feasibility (a constraint is already broken: two queens
attack each other); order (sort the candidates and break at the first one that is too large,
as in "Combination Sum"); bound (the partial cost is already worse than the best solution so
far: branch and bound). With repeated input values, sort the input, and at each level skip a
candidate equal to the previous one (i > start and nums[i] == nums[i-1]).
When dynamic programming applies #
DP applies when a problem has two properties:
- optimal substructure: an optimal solution is built from optimal solutions of smaller problems of the same form (for counting: the count is a sum of smaller counts);
- overlapping subproblems: a plain recursion meets the same smaller problem many times.
The DP script. Say these four things before you code:
- State: what
dp[i](ordp[i][j]) means, in one sentence. - Recurrence: how
dp[i]follows from smaller states, and why its cases cover every possible last choice. - Base cases and order: the smallest states, and a loop order in which every state comes after the states it needs.
- Answer and cost: where the answer is (
dp[n], ormax(dp)), and the cost = number of states × work per state. Then ask: can I keep only the last row?
Memoization is the recursion plus a cache: before computing a state, look it up; after computing it, store it.
Theorem 1 (cost of memoization). If a recursion reaches different states, and each state does at most work and makes at most calls, then with a memo it costs time and memory, plus the recursion depth.
Proof.
- The body runs at most once per state; every later call returns the stored value in (a hash lookup).
- So the bodies cost at most in total, the calls they make cost at most lookups, and the memo holds values.
So the state must be small: its number of possible values is the cost.
From recursion to table to one row #
Top-down is the recurrence plus @functools.cache. It is a good first draft and computes only
reachable states, but Python stops at a recursion depth of about 1,000. Bottom-up fills a table
in a topological order of the DAG of states: no depth limit, and often you can keep only the last
row or the last two values. Four classic 1-D examples:
| Problem | State | Recurrence | Base | Example |
|---|---|---|---|---|
| Climbing Stairs (1 or 2 steps) | = ways to reach step | : 8 ways | ||
| House Robber (no two neighbours) | = best sum from the first houses | , | : 12 | |
| Coin Change (fewest coins) | = fewest coins for amount | ; unreachable | coins , : 2 | |
| Decode Ways ("1"→A … "26"→Z) | = decodings of the suffix from | one digit (not "0") + two digits in 10..26 | "226": 3 |
The first two only look back 1 or 2 steps, so two variables replace the table: memory.
"Coin Change: given coin values and an amount, return the fewest coins that make the amount (each coin can be used many times)." Which state gives a correct and small DP?
The answer for an amount does not depend on how you reached it, so is the state: over coins , with , and time for coin values. The first state misses the amount still to pay, so it cannot be combined. The largest coin is not enough information. The list of chosen coins is the whole path: that is backtracking, not DP.
State shapes #
| State | Meaning | Examples | Cost |
|---|---|---|---|
| prefix | answer for the first items | House Robber, Word Break, Decode Ways | × choices |
| "ends at " | best structure whose last element is item | longest increasing subsequence, maximum subarray | or |
| amount | answer for the total | Coin Change, knapsack, subset sum | for items |
| two prefixes | answer for the first letters of and the first of | longest common subsequence, edit distance | |
| grid cell | answer for paths that end at the cell | Unique Paths, Minimum Path Sum | |
| interval | answer for the piece | palindromes, Burst Balloons | to |
| subset bitmask (+ last item) | answer for a set of used items, | travelling salesman (Held–Karp) |
The amount row is pseudo-polynomial: polynomial in the value , but exponential in its number of bits. (Subset sum is NP-hard; the table is fast only for small amounts.)
Loop order changes the meaning. With a 1-D array over capacities and an item of weight :
for items used at most once (0/1 knapsack), loop over downwards, so that dp[c - w] still
holds the value without this item; for unlimited items, loop upwards. When you count the ways
to make an amount, coins in the outer loop count combinations (each multiset once, because coins
are added in a fixed order), and amounts in the outer loop count sequences ( and
are then two ways).
Interval DP and bitmask DP
Interval DP. The state is a piece of the input, and shorter pieces come first. Longest
palindromic subsequence: if , otherwise
, with and . Fill from down to 0
and from up. time. Example: bbbab gives 4 (bbbb).
Bitmask DP. When , a set of used items fits in an integer of bits, and there are sets. Travelling salesman: = shortest path that starts at item 0, visits exactly the set , and ends at ; . time instead of .
Proving a recurrence: edit distance #
The edit distance (Levenshtein distance) between strings and is the smallest number of insertions, deletions and substitutions of single letters that turn into . Write for the -th letter (counting from 1), and for the lengths, and for the edit distance between the first letters of and the first letters of .
Picture an edit as an alignment: above , with gaps - so that both rows have the same
length and no column has two gaps. A column costs 0 if and 1 otherwise; a column
is a deletion and an insertion. The cheapest alignment costs exactly the edit
distance (Wagner and Fischer, 1974).
s t o p -
- t o p s cost: 1 (delete s) + 0 + 0 + 0 + 1 (insert s) = 2
Theorem 2 (edit-distance recurrence). , , and for
where is 1 if the letters differ and 0 if they are equal.
Proof.
- Base cases. Aligning letters with the empty string needs columns : cost . In the same way .
- Case analysis on the last column. An alignment of the two prefixes ends with , with or with . There is no other case: the last column contains , or , or both.
- Cut and paste. Remove the last column. The rest aligns the prefixes of lengths , or . In an optimal alignment the rest is optimal too: if a cheaper rest existed, putting the same last column after it would give a cheaper alignment.
- So the optimal cost is the cost of the last column plus of the remaining prefixes, for the best of the three cases. Each case can be realized, so the minimum is reached.
The same case analysis on the last column gives the recurrence of the longest common subsequence (LCS): if , otherwise .
Recovering the solution (traceback). The table stores costs, not choices. To get the edits, walk back from : at each cell, move to a neighbour (up, left or diagonal) that gives the minimum in the recurrence, and record the edit of that move. Keeping only two rows saves memory but loses this path; Hirschberg's algorithm (1975) recovers it in time and linear memory.
DP is a shortest path in a DAG. Make one node per cell . A step down is a deletion (cost 1), a step right an insertion (cost 1), a diagonal step a match (0) or a substitution (1). is the shortest path from to in this edit graph, and the row-by-row fill is the "relax in topological order" algorithm of lesson 03.
Fill the table cell by cell. Each cell shows , and its colour says which case won: aqua = match (diagonal, free), orange = substitute (diagonal, +1), blue = delete (from above, +1), violet = insert (from the left, +1). Turn on Predict to type each value before it is shown. When the table is full, Traceback draws the path of one optimal alignment. Edit the two words to try your own.
In an edit-distance table, , and , and the letters and are different. What is ?
The three cases give (delete), (insert) and (substitute, because the letters differ). The minimum is 2. The answer 1 would be right only if the letters were equal (a free match). Taking the minimum of the three neighbours without adding the costs is a common slip.
Greedy or DP? #
A greedy algorithm makes the locally best choice and never revisits it (lesson 02). It is correct only when an exchange argument shows that the greedy choice is part of some optimal solution. For coins it fails in general: with coins and amount 6, greedy takes the largest coin first, (3 coins), but uses 2. Greedy works for "canonical" coin systems such as , but an interviewer expects the DP unless you can prove the greedy choice. A counterexample is the fastest way to say "greedy fails".
For which coin system and amount does "always take the largest coin that fits" give more coins than the optimum?
Greedy takes coins, but needs only 2. In the other systems greedy is optimal for these amounts: (3 coins), (2 coins), (4 coins). These systems are "canonical", so greedy is optimal for every amount there.
Longest increasing subsequence #
A subsequence keeps some items in their original order (not necessarily next to each other). The longest increasing subsequence (LIS) is the longest one whose values strictly increase. The state " = length of the LIS that ends at item " gives (or 1), and the answer is : time.
A different state is faster. After reading a prefix of the input, let be the
smallest last value of a strictly increasing subsequence of length . The LIS length is
the length of tails.
Lemma (the tails array). tails is strictly increasing. A new value changes only the first
position with (found with bisect_left): it sets ,
or appends if there is no such .
Proof of the lemma
- Sorted. Take an increasing subsequence of length that ends at and drop its last element. What is left has length and ends at a smaller value, so .
- Which lengths can end. A subsequence of length that ends at needs one of length
that ends below : either , or . Since
tailsis sorted, this holds exactly for , where is the first position with . - Update. For , already, so nothing changes. For , ends a subsequence of length and (if that entry exists), so position becomes . No longer length is possible (step 2).
One binary search per value gives . bisect_left gives the strictly increasing version;
bisect_right gives the non-decreasing one. Note that tails is in general not a subsequence of
the input: for it ends as .
Predict where each new value goes, then check. Click the slot of tails that the new value
replaces, or the empty slot at the end to append it. The
orange cells are the entries that binary search compared with , and the
aqua cell is the entry that changed. Notice that a small value can
replace an early entry without changing the length: it only makes future extensions easier.
Input size tells you the allowed cost #
A rough guide for Python, which does about simple steps per second:
| up to | Allowed time | Typical method |
|---|---|---|
| 10 | permutations by backtracking | |
| 20 | subsets by backtracking, bitmask DP | |
| 200 | interval DP | |
| 3,000 | two-prefix DP, LIS | |
| or | sorting, tails LIS, 1-D DP |
Answers to section 1. (1) The plain recursion makes calls with
, so . For :
calls. With a memo there are
41 states; each of the 39 states with makes its 2 calls once, plus the first call: 79 calls
(Theorem 1). (2) Listing: no. Every leaf is a different subset, and the output alone holds
numbers; a memo cannot make the output shorter. Deciding the split: yes. "Can the
suffix from index be split?" has the same answer however you reached , so there are only
states. Without a memo the recursion is exponential (try "aaa…ab" with the words "a",
"aa", "aaa").
4. Worked example #
(a) Coin Change with the script. Coins , amount 6, fewest coins.
- State: = the fewest coins that make the amount .
- Recurrence: the last coin is 1, 3 or 4, so , using only the coins . These three cases cover every possible last coin.
- Base cases and order: . Fill upwards: each needs only smaller amounts.
- Answer and cost: ; states × 3 coins = .
| from coin 1 | from coin 3 | from coin 4 | ||
|---|---|---|---|---|
| 0 | — | — | — | 0 |
| 1 | — | — | 1 | |
| 2 | — | — | 2 | |
| 3 | — | 1 | ||
| 4 | 1 | |||
| 5 | 2 | |||
| 6 | 2 |
So , reached from coin 3 at , then coin 3 at : the coins are . Greedy gives , 3 coins.
(b) Edit distance from stop to tops, with the script.
- State: = edit distance between the first letters of
stopand the first letters oftops. - Recurrence: Theorem 2.
- Base cases and order: row 0 and column 0 are . Fill row by row, left to right: each cell needs the cells above, to the left, and up-left.
- Answer and cost: ; cells of work.
| "" | t | o | p | s | |
|---|---|---|---|---|---|
| "" | 0 | 1 | 2 | 3 | 4 |
| s | 1 | 1 | 2 | 3 | 3 |
| t | 2 | 1 | 2 | 3 | 4 |
| o | 3 | 2 | 1 | 2 | 3 |
| p | 4 | 3 | 2 | 1 | 2 |
Three cells in detail:
- ,
sagainstt, different letters: (substitute). - ,
sagainsts, equal letters: . - ,
tagainstt, equal letters: .
So the distance is . Traceback from , p against s: the left cell gives
the minimum, , so the last edit is "insert s". At , and
the letters are equal (p, o, t) and the diagonal gives the minimum: three matches. At
we are in column 0: "delete s". The alignment is the one drawn above Theorem 2. Substitutions alone
(the Hamming distance) would cost 4; insertions and deletions let the alignment shift by one place.
5. In code #
Standard-library Python. The code counts the nodes of the 8-queens search, compares the plain and the memoized recursion, and checks both worked examples.
from functools import cache
import math
# 1) Backtracking with pruning: n queens, one queen per row.
def n_queens(n):
cols, diag1, diag2 = set(), set(), set() # attacked columns and diagonals
solutions, nodes = 0, 0
def go(row):
nonlocal solutions, nodes
nodes += 1 # a node of the recursion tree
if row == n:
solutions += 1
return
for c in range(n):
if c in cols or row - c in diag1 or row + c in diag2:
continue # prune: this square is attacked
cols.add(c); diag1.add(row - c); diag2.add(row + c) # choose
go(row + 1) # explore
cols.remove(c); diag1.remove(row - c); diag2.remove(row + c) # undo
go(0)
return solutions, nodes
print(n_queens(4), n_queens(8), 8 ** 8) # (2, 17) (92, 2057) 16777216
# 2) The same recursion with and without a memo: count how often the body runs.
def fib_runs(n, memo):
runs = 0
def f(k):
nonlocal runs
runs += 1
return k if k < 2 else f(k - 1) + f(k - 2)
if memo:
f = cache(f) # the inner calls now go through the cache
return f(n), runs
print(fib_runs(25, memo=False), fib_runs(25, memo=True)) # (75025, 242785) (75025, 26)
# 3) Coin change (fewest coins), bottom-up, and the greedy answer.
def coin_change(coins, amount):
best = [0] + [math.inf] * amount # best[a] = fewest coins for amount a
for a in range(1, amount + 1):
for c in coins:
if c <= a and best[a - c] + 1 < best[a]:
best[a] = best[a - c] + 1
return best[amount] if best[amount] < math.inf else -1, best
def greedy_coins(coins, amount):
count = 0
for c in sorted(coins, reverse=True): # largest coin first
count += amount // c
amount %= c
return count if amount == 0 else -1
print(coin_change([1, 3, 4], 6)) # (2, [0, 1, 2, 1, 1, 2, 2])
print(greedy_coins([1, 3, 4], 6)) # 3 (4 + 1 + 1), but 3 + 3 uses 2 coins
# 4) Edit distance: the full table of the worked example.
def edit_table(a, b):
m, n = len(a), len(b)
D = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(m + 1):
D[i][0] = i # delete i letters
for j in range(n + 1):
D[0][j] = j # insert j letters
for i in range(1, m + 1):
for j in range(1, n + 1):
D[i][j] = min(D[i - 1][j] + 1, # delete a[i-1]
D[i][j - 1] + 1, # insert b[j-1]
D[i - 1][j - 1] + (a[i - 1] != b[j - 1])) # match or substitute
return D
for row in edit_table("stop", "tops"):
print(row)
# [0, 1, 2, 3, 4]
# [1, 1, 2, 3, 3]
# [2, 1, 2, 3, 4]
# [3, 2, 1, 2, 3]
# [4, 3, 2, 1, 2]
runs without the memo against 26 with it. On long
inputs (more than about 1,000 levels), the memoized version fails with RecursionError and the
bottom-up table does not; this is why interviewers often ask for the table.
6. Where it shows up #
- Speech and sequences. The word error rate (WER) is a word-level edit distance divided by the number of reference words. The CTC loss (Graves et al., 2006) sums over all alignments with a DP over (time step, position in the label). The forward algorithm of a hidden Markov model sums over paths and Viterbi takes the maximum, on the same (time, state) table.
- Reinforcement learning. The Bellman optimality equation (value , state , action , reward , discount , next state ) is a DP recurrence over states; value iteration fills its table. See the reinforcement learning track.
- Decoding. Beam search keeps the best partial outputs at each step: a backtracking search with aggressive pruning, so it can miss the most likely sequence.
- ML for science. DNA and protein alignment (Needleman–Wunsch, 1970; Smith–Waterman, 1981) is the edit-distance table with a substitution score matrix and gap penalties. Dynamic time warping (DTW) aligns two time series that run at different speeds: .
- Solvers. SAT solvers descend from DPLL (1962), a backtracking search with pruning; mixed-integer programming solvers use branch and bound.
7. Common mistakes #
- Recording
curinstead of a copy (cur[:]): all stored lists are one object, empty at the end. - Forgetting to undo a choice (
pop,used[i] = False): the state leaks into the next branch. - Duplicates by order: a loop from 0 instead of from the start index gives and as two combinations.
- A state that misses information (on a circular street: was the first house robbed?): the memo then returns wrong values.
- Wrong base case or answer cell. Counting:
ways[0] = 1(one way to make nothing). Minimum: unreachable is , and a final becomes what the task asks (often-1). "Ends at ": the answer ismax(dp), notdp[-1]. - Wrong loop order (combinations or sequences; capacity downwards for 0/1 items).
- Assuming greedy works without a proof or a quick search for a counterexample.
- Python:
@cacheneeds hashable arguments (indices, not lists); large inputs need the table because of the recursion limit.
8. Check yourself #
- How many nodes does the permutation tree have for ? Compare with .
Answer
, and . In fact the count is for every . - Coins , amount 4. How many combinations and how many sequences? Which loop nesting
counts which?
Answer
3 combinations (, , ) and 5 sequences (, , , , ). Coins in the outer loop count combinations; amounts in the outer loop count sequences. - Give the LCS length and the edit distance of
stopandtops. Why is the edit distance never more than , where is the LCS length?Answer
(top) and the edit distance is 2. A common subsequence of length gives an edit script: delete the other letters of and insert the other letters of . So the distance is at most ; here , and the bound is reached. - Run the tails method on . Give
tailsafter each step and the LIS length. Is the finaltailsa subsequence of the input?Answer
. Length 4, for example . The finaltailsis not a subsequence: the 2 comes after the 5 in the input. - "Count the subsets of with sum 50" and "list them". Which method for each, and
at what cost?
Answer
Count with DP over the sum:ways[s], one downward loop over per item, , here updates; the count is 1,969. List with backtracking (take or skip, prune when the sum passes 50): at least the size of the output, 1,969 subsets times their length. - Longest palindromic subsequence of a string : give the state, the recurrence, the base cases,
the order and the cost.
Answer
= length of the longest palindromic subsequence of . If : ; otherwise . Base: and (empty). Order: from down to 0, from up (shorter pieces first). Answer ; time, memory with two rows. Example:bbbabgives 4. It also equals the LCS of and its reverse.
What are the three steps of the backtracking template, and what must you record at a leaf?
Choose (apply the choice), explore (recurse), undo (revert the choice). Record a copy of the current solution, for example
cur[:].How do you bound the running time of a backtracking search?
Number of nodes in the recursion tree × work per node, plus the cost of copying each solution.
When does dynamic programming apply?
When the answer below a node depends only on a small state (optimal substructure) and the same states come back many times (overlapping subproblems).
Name the four parts of the DP script.
State (what
dp[i]means), recurrence (with every last choice as a case), base cases and fill order, answer and cost.What does a memoized recursion cost?
time and memory for states with work each, plus the recursion depth.
How many calls does the plain recursive
fib(n)make?; for example and . With a memo: calls.
Write the edit-distance recurrence.
, with and .
Write the Coin Change (fewest coins) recurrence.
over the coin values , with and for unreachable amounts.
What is the invariant of the tails method for the LIS, and what does it cost?
tails[k]is the smallest last value of an increasing subsequence of length , andtailsis strictly increasing; one binary search per item gives .Give a counterexample to greedy coin change.
Coins , amount 6: greedy gives (3 coins), the optimum is (2 coins).
9. Interview angle #
Recognize the pattern from the wording:
| The problem says | Try |
|---|---|
| "all", "every", "generate", "list" (subsets, permutations, combinations) | backtracking: choose, explore, undo |
| "place without conflict", "fill the board" (queens, sudoku) | backtracking with feasibility pruning |
| "how many ways", "minimum", "maximum", "is it possible" | DP: pick a state shape from the table in section 3 |
| a budget, an amount, a capacity | DP over the amount (knapsack, coin change) |
| two strings or two sequences | DP over two prefixes (LCS, edit distance) |
| a range or a piece of a string, "burst", "merge" | interval DP |
| exponential time is allowed: backtracking or bitmask DP | |
| "longest increasing" | DP, then tails with binary search |
| "is greedy enough?" | look for a counterexample; otherwise prove an exchange argument |
The routine to say aloud. Brute-force recursion first ("try every last choice"). Then: "the same arguments come back, and the answer depends only on these arguments", so name the state. Say the script (state, recurrence, base cases and order, answer and cost). Write the memo version (the recurrence as code). Convert to a table if the depth can exceed 1,000, and keep fewer rows if memory matters. Test on the smallest inputs: empty string, amount 0, one item.
Typical questions and short model answers.
- "What is the difference between memoization and tabulation?" — Both compute each state once. Memoization is top-down: the recursion with a cache; it is quick to write and computes only the states it reaches, but it uses the call stack, so Python fails beyond about 1,000 levels. Tabulation is bottom-up: I fill the table in an order where every state comes after the states it needs. It has no recursion limit and lets me keep only the last row. The cost is the same: number of states × work per state.
- "Why is your recurrence correct?" (edit distance) — I look at the last column of an optimal alignment. It deletes , inserts , or pairs them, at cost 0 or 1. There is no other case. The rest of the alignment must be optimal for the shorter prefixes, otherwise I could replace it and get a cheaper whole. So the answer is the minimum over the three cases.
- "Can you do it in less memory?" — Each row needs only the previous row, so two rows, . For 1-D problems that look back a fixed number of steps (stairs, robber), two variables, . To also return the edits with linear memory, Hirschberg's divide and conquer.
- "Why not greedy?" — I give a counterexample: coins , amount 6; greedy uses 3 coins, the optimum 2.
- "What is the complexity of generating all permutations?" — The tree has fewer than nodes, each scanning candidates, and each of the leaves copies items: . This is optimal up to a constant, because the output itself has numbers.
Typical follow-ups. Return one optimal solution (traceback, or store the choice per state). Duplicates in the input (sort, skip equal siblings). Count instead of list (backtracking becomes DP). Circular input (House Robber II: run twice, without the first or without the last house). Very large amount (pseudo-polynomial: the table is too big; ask about constraints).
10. Practice #
The notebook for this lesson runs in Colab. Each exercise has a check cell that compares your code with a brute force on random inputs, and a folded solution:
subsets(nums): backtracking; checked againstitertools.combinationsand the count .coin_change(coins, amount): fewest coins or-1; checked against a BFS over amounts.word_break(s, words): DP over prefixes; checked against a brute-force recursion.edit_distance(a, b): the table plus a traceback that returns an alignment; the check verifies the cost and that the alignment is valid.lis_tails(nums): the method; checked against the DP on random arrays.
Timed practice (25 minutes each, talking aloud): Subsets II, Permutations, Combination Sum, N-Queens, House Robber II, Coin Change II, Longest Common Subsequence, Edit Distance, Partition Equal Subset Sum, Longest Increasing Subsequence.
Say it aloud (2 minutes, no notes): the DP script for Coin Change, then the proof of the edit-distance recurrence by case analysis on the last column.
11. Watch and read #
- Jeff Erickson, Algorithms, free PDF: ch. 2 (backtracking: n queens, subset sum, the general pattern), ch. 3 (dynamic programming: Fibonacci, memoization, longest increasing subsequence, edit distance, "greed is stupid"), §6.4 (dynamic programming as depth-first search in a DAG).
- cp-algorithms, Longest increasing subsequence: the DP, the method with binary search, and how to restore the subsequence.
- MIT 6.006 Spring 2020, lecture notes and recitations (the course of the first two videos; recitations 15–17 have many solved DP problems).
- Problem lists: Grind 75 (topics Recursion, Dynamic Programming) and NeetCode 150 (sections Backtracking, 1-D Dynamic Programming, 2-D Dynamic Programming).
Runs in your browser with nothing to install. The notebook repeats the key demos and has exercises with checks; solutions are inside, folded.
No Google account needed: Python runs inside your browser. The first start downloads about 40 MB; your changes are saved in this browser only.