Type at least 2 letters. Enter opens the first result, Esc closes.

    ML Interview Prep · Lesson 4 · Coding rounds

    Backtracking and dynamic programming

    Intermediate 80 min 4 interactive figures 4 videos Colab notebook

    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. nn is the input size (number of items or length of a string); mm and nn are the lengths of two strings; AA 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 {1,2}\{1, 2\}: at depth kk you skip or take item k+1k+1.

                     []
           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 88≈1.7⋅1078^8 \approx 1.7 \cdot 10^7 complete boards without pruning.

    Interactive figure: loading…

    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 f(k)f(k) with k≥2k \ge 2 points to f(k−1)f(k-1) and f(k−2)f(k-2): a small DAG of states (directed acyclic graph, lesson 03).

    Interactive figure: loading…

    Move the slider and switch the view. Node kk is the call f(k)f(k), 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 C(n)=2F(n+1)−1C(n) = 2F(n+1) - 1 calls (FF = Fibonacci numbers), which grows like 1.618n1.618^n; the DAG has only n+1n + 1 states and 2(n−1)2(n-1) 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.

    Quick check

    How many calls (including the first one) does the plain recursive fib(4) make?

    Quick check

    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
    

    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 2n2^n O(n 2n)O(n\,2^n)
    combinations of size kk the next item, with index larger than the last one a start index (nk)\binom{n}{k} O(k(nk))O(k \binom{n}{k}) if you stop when too few items are left
    permutations any unused item a used array n!n! O(n⋅n!)O(n \cdot n!)

    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 nn items has ∑k=0nn!(n−k)!<e⋅n!\sum_{k=0}^{n} \frac{n!}{(n-k)!} < e \cdot n! nodes.

    Proof.

    1. A node at depth kk is an ordered choice of kk different items out of nn. There are n(n−1)⋯(n−k+1)=n!(n−k)!n (n-1) \cdots (n-k+1) = \frac{n!}{(n-k)!} of them.
    2. Sum over kk and write j=n−kj = n - k: ∑j=0nn!j!=n!∑j=0n1j!<e⋅n!\sum_{j=0}^{n} \frac{n!}{j!} = n! \sum_{j=0}^{n} \frac{1}{j!} < e \cdot n!, because ∑j≥01/j!=e\sum_{j \ge 0} 1/j! = e. ■\blacksquare

    Each node scans nn candidates and each leaf copies nn items, so the total is O(n⋅n!)O(n \cdot n!).

    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:

    1. State: what dp[i] (or dp[i][j]) means, in one sentence.
    2. Recurrence: how dp[i] follows from smaller states, and why its cases cover every possible last choice.
    3. Base cases and order: the smallest states, and a loop order in which every state comes after the states it needs.
    4. Answer and cost: where the answer is (dp[n], or max(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 SS different states, and each state does at most WW work and makes at most WW calls, then with a memo it costs O(S⋅W)O(S \cdot W) time and O(S)O(S) memory, plus the recursion depth.

    Proof.

    1. The body runs at most once per state; every later call returns the stored value in O(1)O(1) (a hash lookup).
    2. So the bodies cost at most S⋅WS \cdot W in total, the calls they make cost at most S⋅WS \cdot W lookups, and the memo holds SS values. ■\blacksquare

    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) w[i]w[i] = ways to reach step ii w[i]=w[i−1]+w[i−2]w[i] = w[i-1] + w[i-2] w[0]=w[1]=1w[0] = w[1] = 1 n=5n = 5: 8 ways
    House Robber (no two neighbours) r[i]r[i] = best sum from the first ii houses r[i]=max⁡(r[i−1], r[i−2]+hi)r[i] = \max(r[i-1],\ r[i-2] + h_i) r[0]=0r[0] = 0, r[1]=h1r[1] = h_1 [2,7,9,3,1][2,7,9,3,1]: 12
    Coin Change (fewest coins) c[a]c[a] = fewest coins for amount aa c[a]=1+min⁡coin k≤ac[a−k]c[a] = 1 + \min_{\text{coin } k \le a} c[a-k] c[0]=0c[0] = 0; unreachable =∞= \infty coins {1,3,4}\{1,3,4\}, a=6a = 6: 2
    Decode Ways ("1"→A … "26"→Z) d[i]d[i] = decodings of the suffix from ii one digit (not "0") + two digits in 10..26 d[n]=1d[n] = 1 "226": 3

    The first two only look back 1 or 2 steps, so two variables replace the table: O(1)O(1) memory.

    Quick check

    "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?

    State shapes #

    State Meaning Examples Cost
    prefix ii answer for the first ii items House Robber, Word Break, Decode Ways O(n)O(n) × choices
    "ends at ii" best structure whose last element is item ii longest increasing subsequence, maximum subarray O(n2)O(n^2) or O(n)O(n)
    amount aa answer for the total aa Coin Change, knapsack, subset sum O(A⋅k)O(A \cdot k) for kk items
    two prefixes (i,j)(i, j) answer for the first ii letters of aa and the first jj of bb longest common subsequence, edit distance O(mn)O(mn)
    grid cell (r,c)(r, c) answer for paths that end at the cell Unique Paths, Minimum Path Sum O(RC)O(RC)
    interval [i,j][i, j] answer for the piece si…sjs_i \dots s_j palindromes, Burst Balloons O(n2)O(n^2) to O(n3)O(n^3)
    subset bitmask (+ last item) answer for a set of used items, n≤20n \le 20 travelling salesman (Held–Karp) O(2nn2)O(2^n n^2)

    The amount row is pseudo-polynomial: polynomial in the value AA, 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 cc and an item of weight ww: for items used at most once (0/1 knapsack), loop over cc 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 (1+21{+}2 and 2+12{+}1 are then two ways).

    Interval DP and bitmask DP

    Interval DP. The state is a piece [i,j][i, j] of the input, and shorter pieces come first. Longest palindromic subsequence: P(i,j)=P(i+1,j−1)+2P(i, j) = P(i+1, j-1) + 2 if si=sjs_i = s_j, otherwise max⁡(P(i+1,j),P(i,j−1))\max(P(i+1, j), P(i, j-1)), with P(i,i)=1P(i, i) = 1 and P(i+1,i)=0P(i+1, i) = 0. Fill ii from n−1n-1 down to 0 and jj from i+1i+1 up. O(n2)O(n^2) time. Example: bbbab gives 4 (bbbb).

    Bitmask DP. When n≤20n \le 20, a set of used items fits in an integer of nn bits, and there are 2n≈1062^n \approx 10^6 sets. Travelling salesman: T(S,v)T(S, v) = shortest path that starts at item 0, visits exactly the set SS, and ends at vv; T(S,v)=min⁡u∈S∖{v}T(S∖{v},u)+d(u,v)T(S, v) = \min_{u \in S \setminus \{v\}} T(S \setminus \{v\}, u) + d(u, v). O(2nn2)O(2^n n^2) time instead of O(n!)O(n!).

    Proving a recurrence: edit distance #

    The edit distance (Levenshtein distance) between strings aa and bb is the smallest number of insertions, deletions and substitutions of single letters that turn aa into bb. Write aia_i for the ii-th letter (counting from 1), mm and nn for the lengths, and D(i,j)D(i, j) for the edit distance between the first ii letters of aa and the first jj letters of bb.

    Picture an edit as an alignment: aa above bb, with gaps - so that both rows have the same length and no column has two gaps. A column (x,y)(x, y) costs 0 if x=yx = y and 1 otherwise; a column (ai,−)(a_i, -) is a deletion and (−,bj)(-, b_j) 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). D(i,0)=iD(i, 0) = i, D(0,j)=jD(0, j) = j, and for i,j≥1i, j \ge 1

    D(i,j)=min⁡(D(i−1,j)+1,  D(i,j−1)+1,  D(i−1,j−1)+[ai≠bj]),D(i, j) = \min\big(D(i-1, j) + 1,\; D(i, j-1) + 1,\; D(i-1, j-1) + [a_i \ne b_j]\big),

    where [ai≠bj][a_i \ne b_j] is 1 if the letters differ and 0 if they are equal.

    Proof.

    1. Base cases. Aligning ii letters with the empty string needs ii columns (ak,−)(a_k, -): cost ii. In the same way D(0,j)=jD(0, j) = j.
    2. Case analysis on the last column. An alignment of the two prefixes ends with (ai,−)(a_i, -), with (−,bj)(-, b_j) or with (ai,bj)(a_i, b_j). There is no other case: the last column contains aia_i, or bjb_j, or both.
    3. Cut and paste. Remove the last column. The rest aligns the prefixes of lengths (i−1,j)(i-1, j), (i,j−1)(i, j-1) or (i−1,j−1)(i-1, j-1). 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.
    4. So the optimal cost is the cost of the last column plus DD of the remaining prefixes, for the best of the three cases. Each case can be realized, so the minimum is reached. ■\blacksquare

    The same case analysis on the last column gives the recurrence of the longest common subsequence (LCS): L(i,j)=L(i−1,j−1)+1L(i, j) = L(i-1, j-1) + 1 if ai=bja_i = b_j, otherwise max⁡(L(i−1,j),L(i,j−1))\max(L(i-1, j), L(i, j-1)).

    Recovering the solution (traceback). The table stores costs, not choices. To get the edits, walk back from (m,n)(m, n): 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 O(mn)O(mn) time and linear memory.

    DP is a shortest path in a DAG. Make one node per cell (i,j)(i, j). 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). D(m,n)D(m, n) is the shortest path from (0,0)(0, 0) to (m,n)(m, n) in this edit graph, and the row-by-row fill is the "relax in topological order" algorithm of lesson 03.

    Interactive figure: loading…

    Fill the table cell by cell. Each cell shows D(i,j)D(i, j), 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.

    Quick check

    In an edit-distance table, D(i−1,j)=2D(i-1, j) = 2, D(i,j−1)=3D(i, j-1) = 3 and D(i−1,j−1)=1D(i-1, j-1) = 1, and the letters aia_i and bjb_j are different. What is D(i,j)D(i, j)?

    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 {1,3,4}\{1, 3, 4\} and amount 6, greedy takes the largest coin first, 4+1+14 + 1 + 1 (3 coins), but 3+33 + 3 uses 2. Greedy works for "canonical" coin systems such as {1,2,5,10}\{1, 2, 5, 10\}, but an interviewer expects the DP unless you can prove the greedy choice. A counterexample is the fastest way to say "greedy fails".

    Quick check

    For which coin system and amount does "always take the largest coin that fits" give more coins than the optimum?

    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 "end[i]\text{end}[i] = length of the LIS that ends at item ii" gives end[i]=1+max⁡{end[j]:j<i, xj<xi}\text{end}[i] = 1 + \max\{\text{end}[j] : j < i,\ x_j < x_i\} (or 1), and the answer is max⁡iend[i]\max_i \text{end}[i]: O(n2)O(n^2) time.

    A different state is faster. After reading a prefix of the input, let tails[k]\text{tails}[k] be the smallest last value of a strictly increasing subsequence of length k+1k + 1. The LIS length is the length of tails.

    Lemma (the tails array). tails is strictly increasing. A new value xx changes only the first position kk with tails[k]≥x\text{tails}[k] \ge x (found with bisect_left): it sets tails[k]=x\text{tails}[k] = x, or appends xx if there is no such kk.

    Proof of the lemma
    1. Sorted. Take an increasing subsequence of length k+2k + 2 that ends at tails[k+1]\text{tails}[k+1] and drop its last element. What is left has length k+1k + 1 and ends at a smaller value, so tails[k]<tails[k+1]\text{tails}[k] < \text{tails}[k+1].
    2. Which lengths xx can end. A subsequence of length j+1j + 1 that ends at xx needs one of length jj that ends below xx: either j=0j = 0, or tails[j−1]<x\text{tails}[j-1] < x. Since tails is sorted, this holds exactly for j≤kj \le k, where kk is the first position with tails[k]≥x\text{tails}[k] \ge x.
    3. Update. For j<kj < k, tails[j]<x\text{tails}[j] < x already, so nothing changes. For j=kj = k, xx ends a subsequence of length k+1k + 1 and x≤tails[k]x \le \text{tails}[k] (if that entry exists), so position kk becomes xx. No longer length is possible (step 2). ■\blacksquare

    One binary search per value gives O(nlog⁡n)O(n \log n). 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 [3,4,1][3, 4, 1] it ends as [1,4][1, 4].

    Interactive figure: loading…

    Predict where each new value goes, then check. Click the slot of tails that the new value xx replaces, or the empty slot at the end to append it. The orange cells are the entries that binary search compared with xx, 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 10710^7 simple steps per second:

    nn up to Allowed time Typical method
    10 O(n!)O(n!) permutations by backtracking
    20 O(n 2n)O(n\,2^n) subsets by backtracking, bitmask DP
    200 O(n3)O(n^3) interval DP
    3,000 O(n2)O(n^2) two-prefix DP, O(n2)O(n^2) LIS
    10610^6 O(nlog⁡n)O(n \log n) or O(n)O(n) sorting, tails LIS, 1-D DP

    Answers to section 1. (1) The plain recursion makes C(n)=1+C(n−1)+C(n−2)C(n) = 1 + C(n-1) + C(n-2) calls with C(0)=C(1)=1C(0) = C(1) = 1, so C(n)=2F(n+1)−1C(n) = 2F(n+1) - 1. For n=40n = 40: 2⋅165,580,141−1=331,160,281≈3⋅1082 \cdot 165{,}580{,}141 - 1 = 331{,}160{,}281 \approx 3 \cdot 10^8 calls. With a memo there are 41 states; each of the 39 states with n≥2n \ge 2 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 n 2n−1n \, 2^{n-1} numbers; a memo cannot make the output shorter. Deciding the split: yes. "Can the suffix from index ii be split?" has the same answer however you reached ii, so there are only n+1n + 1 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 {1,3,4}\{1, 3, 4\}, amount 6, fewest coins.

    1. State: c[a]c[a] = the fewest coins that make the amount aa.
    2. Recurrence: the last coin is 1, 3 or 4, so c[a]=1+min⁡(c[a−1],c[a−3],c[a−4])c[a] = 1 + \min(c[a-1], c[a-3], c[a-4]), using only the coins ≤a\le a. These three cases cover every possible last coin.
    3. Base cases and order: c[0]=0c[0] = 0. Fill a=1,2,…,6a = 1, 2, \dots, 6 upwards: each c[a]c[a] needs only smaller amounts.
    4. Answer and cost: c[6]c[6]; 77 states × 3 coins = O(A⋅k)O(A \cdot k).
    aa from coin 1 from coin 3 from coin 4 c[a]c[a]
    0 — — — 0
    1 c[0]+1=1c[0] + 1 = 1 — — 1
    2 c[1]+1=2c[1] + 1 = 2 — — 2
    3 c[2]+1=3c[2] + 1 = 3 c[0]+1=1c[0] + 1 = 1 — 1
    4 c[3]+1=2c[3] + 1 = 2 c[1]+1=2c[1] + 1 = 2 c[0]+1=1c[0] + 1 = 1 1
    5 c[4]+1=2c[4] + 1 = 2 c[2]+1=3c[2] + 1 = 3 c[1]+1=2c[1] + 1 = 2 2
    6 c[5]+1=3c[5] + 1 = 3 c[3]+1=2c[3] + 1 = 2 c[2]+1=3c[2] + 1 = 3 2

    So c[6]=2c[6] = 2, reached from coin 3 at a=6a = 6, then coin 3 at a=3a = 3: the coins are 3+33 + 3. Greedy gives 4+1+14 + 1 + 1, 3 coins.

    (b) Edit distance from stop to tops, with the script.

    1. State: D(i,j)D(i, j) = edit distance between the first ii letters of stop and the first jj letters of tops.
    2. Recurrence: Theorem 2.
    3. Base cases and order: row 0 and column 0 are 0,1,2,3,40, 1, 2, 3, 4. Fill row by row, left to right: each cell needs the cells above, to the left, and up-left.
    4. Answer and cost: D(4,4)D(4, 4); 5×5=255 \times 5 = 25 cells of O(1)O(1) 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:

    • (1,1)(1, 1), s against t, different letters: min⁡(D(0,1)+1, D(1,0)+1, D(0,0)+1)=min⁡(2,2,1)=1\min(D(0,1) + 1,\ D(1,0) + 1,\ D(0,0) + 1) = \min(2, 2, 1) = 1 (substitute).
    • (1,4)(1, 4), s against s, equal letters: min⁡(D(0,4)+1, D(1,3)+1, D(0,3)+0)=min⁡(5,4,3)=3\min(D(0,4) + 1,\ D(1,3) + 1,\ D(0,3) + 0) = \min(5, 4, 3) = 3.
    • (2,1)(2, 1), t against t, equal letters: min⁡(D(1,1)+1, D(2,0)+1, D(1,0)+0)=min⁡(2,3,1)=1\min(D(1,1) + 1,\ D(2,0) + 1,\ D(1,0) + 0) = \min(2, 3, 1) = 1.

    So the distance is D(4,4)=2D(4, 4) = 2. Traceback from (4,4)(4, 4), p against s: the left cell gives the minimum, D(4,3)+1=2D(4, 3) + 1 = 2, so the last edit is "insert s". At (4,3)(4, 3), (3,2)(3, 2) and (2,1)(2, 1) the letters are equal (p, o, t) and the diagonal gives the minimum: three matches. At (1,0)(1, 0) 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]
    

    2F(26)−1=2⋅121,393−1=242,7852F(26) - 1 = 2 \cdot 121{,}393 - 1 = 242{,}785 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 V(s)=max⁡a(r(s,a)+γ E[V(s′)])V(s) = \max_a \big(r(s, a) + \gamma\, \mathbb{E}[V(s')]\big) (value VV, state ss, action aa, reward rr, discount γ\gamma, next state s′s') is a DP recurrence over states; value iteration fills its table. See the reinforcement learning track.
    • Decoding. Beam search keeps the best kk 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: D(i,j)=∣xi−yj∣+min⁡(D(i−1,j),D(i,j−1),D(i−1,j−1))D(i, j) = |x_i - y_j| + \min\big(D(i-1, j), D(i, j-1), D(i-1, j-1)\big).
    • Solvers. SAT solvers descend from DPLL (1962), a backtracking search with pruning; mixed-integer programming solvers use branch and bound.

    7. Common mistakes #

    • Recording cur instead 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 [2,3][2, 3] and [3,2][3, 2] 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 ∞\infty, and a final ∞\infty becomes what the task asks (often -1). "Ends at ii": the answer is max(dp), not dp[-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: @cache needs hashable arguments (indices, not lists); large inputs need the table because of the recursion limit.

    8. Check yourself #

    1. How many nodes does the permutation tree have for n=4n = 4? Compare with e⋅4!e \cdot 4!.
      Answer1+4+12+24+24=651 + 4 + 12 + 24 + 24 = 65, and e⋅24≈65.2e \cdot 24 \approx 65.2. In fact the count is ⌊e⋅n!⌋\lfloor e \cdot n! \rfloor for every n≥1n \ge 1.
    2. Coins {1,2}\{1, 2\}, amount 4. How many combinations and how many sequences? Which loop nesting counts which?
      Answer3 combinations (1+1+1+11{+}1{+}1{+}1, 1+1+21{+}1{+}2, 2+22{+}2) and 5 sequences (11111111, 112112, 121121, 211211, 2222). Coins in the outer loop count combinations; amounts in the outer loop count sequences.
    3. Give the LCS length and the edit distance of stop and tops. Why is the edit distance never more than m+n−2Lm + n - 2L, where LL is the LCS length?
      AnswerL=3L = 3 (top) and the edit distance is 2. A common subsequence of length LL gives an edit script: delete the other m−Lm - L letters of aa and insert the other n−Ln - L letters of bb. So the distance is at most m+n−2Lm + n - 2L; here 4+4−6=24 + 4 - 6 = 2, and the bound is reached.
    4. Run the tails method on [3,1,4,1,5,9,2,6][3, 1, 4, 1, 5, 9, 2, 6]. Give tails after each step and the LIS length. Is the final tails a subsequence of the input?
      Answer[3]→[1]→[1,4]→[1,4]→[1,4,5]→[1,4,5,9]→[1,2,5,9]→[1,2,5,6][3] \to [1] \to [1, 4] \to [1, 4] \to [1, 4, 5] \to [1, 4, 5, 9] \to [1, 2, 5, 9] \to [1, 2, 5, 6]. Length 4, for example 1,4,5,91, 4, 5, 9. The final tails is not a subsequence: the 2 comes after the 5 in the input.
    5. "Count the subsets of {1,…,20}\{1, \dots, 20\} with sum 50" and "list them". Which method for each, and at what cost?
      AnswerCount with DP over the sum: ways[s], one downward loop over ss per item, O(n⋅S)O(n \cdot S), here 20⋅5120 \cdot 51 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.
    6. Longest palindromic subsequence of a string ss: give the state, the recurrence, the base cases, the order and the cost.
      AnswerP(i,j)P(i, j) = length of the longest palindromic subsequence of si…sjs_i \dots s_j. If si=sjs_i = s_j: P(i,j)=P(i+1,j−1)+2P(i, j) = P(i+1, j-1) + 2; otherwise max⁡(P(i+1,j),P(i,j−1))\max(P(i+1, j), P(i, j-1)). Base: P(i,i)=1P(i, i) = 1 and P(i+1,i)=0P(i+1, i) = 0 (empty). Order: ii from n−1n - 1 down to 0, jj from i+1i + 1 up (shorter pieces first). Answer P(0,n−1)P(0, n-1); O(n2)O(n^2) time, O(n)O(n) memory with two rows. Example: bbbab gives 4. It also equals the LCS of ss and its reverse.
    Recall cardsAnswer in your head first, then reveal. Add the cards to your review deck to see them again just before you would forget.
    1. What are the three steps of the backtracking template, and what must you record at a leaf?

    2. How do you bound the running time of a backtracking search?

    3. When does dynamic programming apply?

    4. Name the four parts of the DP script.

    5. What does a memoized recursion cost?

    6. How many calls does the plain recursive fib(n) make?

    7. Write the edit-distance recurrence.

    8. Write the Coin Change (fewest coins) recurrence.

    9. What is the invariant of the tails method for the LIS, and what does it cost?

    10. Give a counterexample to greedy coin change.

    Open the review →

    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
    n≤20n \le 20 exponential time is allowed: backtracking or bitmask DP
    "longest increasing" O(n2)O(n^2) 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 aia_i, inserts bjb_j, 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, O(n)O(n). For 1-D problems that look back a fixed number of steps (stairs, robber), two variables, O(1)O(1). To also return the edits with linear memory, Hirschberg's divide and conquer.
    • "Why not greedy?" — I give a counterexample: coins {1,3,4}\{1, 3, 4\}, amount 6; greedy uses 3 coins, the optimum 2.
    • "What is the complexity of generating all permutations?" — The tree has fewer than e⋅n!e \cdot n! nodes, each scanning nn candidates, and each of the n!n! leaves copies nn items: O(n⋅n!)O(n \cdot n!). This is optimal up to a constant, because the output itself has n⋅n!n \cdot n! 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:

    1. subsets(nums): backtracking; checked against itertools.combinations and the count 2n2^n.
    2. coin_change(coins, amount): fewest coins or -1; checked against a BFS over amounts.
    3. word_break(s, words): DP over prefixes; checked against a brute-force recursion.
    4. edit_distance(a, b): the table plus a traceback that returns an alignment; the check verifies the cost and that the alignment is valid.
    5. lis_tails(nums): the O(nlog⁡n)O(n \log n) method; checked against the O(n2)O(n^2) 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 #

    The SRTBOT recipe (subproblems, relation, topological order, base, original, time) is the DP script of this lesson.
    Open on YouTube · the player loads from youtube-nocookie.com when you press play
    The prefix and suffix states of the LCS and LIS, and the coin problem, with their recurrences.
    Open on YouTube · the player loads from youtube-nocookie.com when you press play
    The decision tree "take or skip" and the choose, explore, undo template in code.
    Open on YouTube · the player loads from youtube-nocookie.com when you press play
    The greedy counterexample, the recursion tree, and the bottom-up table over amounts.
    Open on YouTube · the player loads from youtube-nocookie.com when you press play
    • 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 O(n2)O(n^2) DP, the O(nlog⁡n)O(n \log n) 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).