Algorithm design · 15 min

Dynamic programming on sequences: one prefix at a time

How a table over prefixes finds the longest common subsequence and the edit distance of two words in O(mn), why listing the letters or edits needs the whole table, and how the longest increasing subsequence works on one sequence in O(n²) or, with binary search, O(n log n).

01Why it matters

How alike are two words?

Type “recieve” and a spell checker offers “receive”. A diff tool lines up two versions of a file and shows only the lines that changed. Both are asking how alike two sequences are: what they share, in order, and how few changes turn one into the other.

Trying every answer is hopeless: a word of m letters has 2m2^m subsequences, about a million for 20 letters and a trillion for 40. The dynamic programming lesson filled a table of smaller answers instead. With two sequences, the smaller answers are about prefixes: the first i letters of one and the first j of the other. One table holds every pair, and each cell costs a single comparison.

02Key parts

The pieces you will see

subsequence and substring
A subsequence keeps some letters in their order and may skip the rest: “hat” is a subsequence of “chart”. A substring is a run of consecutive letters, such as “har”.
prefix
A[1..i], the first i letters of A, counted from 1. The empty prefix, ε, has none; row 0 and column 0 stand for it.
L[i,j] and D[i,j]
What one cell means. L[i,j] is the length of the longest common subsequence (LCS) of A[1..i] and B[1..j]; D[i,j] is the fewest edits that turn A[1..i] into B[1..j].
edit distance
The fewest single-letter edits (insert, delete or replace) that turn one word into another, also called the Levenshtein distance.
traceback
A walk from the table's corner back to its edge, following the neighbour each cell was built from, to recover the letters or edits themselves.

03See it work

One comparison and three neighbours

For the longest common subsequence, the four decisions of dynamic programming are:

  • State: L[i,j] is the length of the LCS of A[1..i] and B[1..j].
  • Base cases: an empty prefix shares nothing, so row 0 and column 0 are all 0.
  • Recurrence: look only at the last letters, A[i] and B[j]. If they match, that letter can end a common subsequence, so extend the best for the shorter prefixes: L[i,j] = L[i−1,j−1] + 1. If they differ, at most one of them can end it, so leave out A[i] (the cell above) or B[j] (the cell to the left) and keep the better: L[i,j] = max(L[i−1,j], L[i,j−1]).
  • Fill order: a cell reads the cells above, to the left and diagonally up-left, so fill row by row, left to right.
DYNAMIC against PANORAMIC: 63 cells, one comparison each. The corner reaches 5, and the walk back reads NAMIC. Its letters sit side by side in DYNAMIC, but in PANORAMIC the walk steps left past R and O, letters the subsequence skips.

The answer is the corner, L[m,n], the one cell that covers both whole words. The letters come from the walk back: a match steps diagonally and keeps its letter; otherwise the walk steps to the larger neighbour (up on a tie).

With a different rule, the same table gives the edit distance, D[i,j]. Turning i letters into nothing takes i deletes, and building j letters from nothing takes j inserts, so row 0 and column 0 count up from 0. For any other cell, think about the last edit of a cheapest script:

  • Delete A[i], after turning A[1..i − 1] into B[1..j]: the cell above, plus 1.
  • Insert B[j], after turning A[1..i] into B[1..j − 1]: the cell to the left, plus 1.
  • Replace A[i] with B[j], after turning A[1..i − 1] into B[1..j − 1]: the diagonal, plus 1.

D[i,j] is the smallest of the three. When A[i] = B[j] there is nothing to replace, so the cell copies the diagonal, which the other two can never beat.

kitten into sitting. Row 0 counts 0 to 7 and column 0 counts 0 to 6. The corner reaches 3, and the walk back reads off the script: replace k with s, keep i, t and t, replace e with i, keep n, insert g.

04Predict

Your turn: stop into tops

Turn “stop” into “tops”. Before each cell, predict its value or its cheapest edit: delete (from above), insert (from the left) or replace (from the diagonal). Then name the edit each step of the walk back stands for.

Make 6 predictions: step forward and answer each question before the next step plays.

0 of 6 made

05Try it

The table in code, and one sequence on its own

Here is the LCS table in code, for “no” and “on”. Strings count from 0, so letter i of a is a[i - 1]. The program makes a table of zeros, then fills it from row 1 and column 1.

def lcs_length(a, b):
    dp = []
    for i in range(len(a) + 1):
        dp.append([0] * (len(b) + 1))
    for i in range(1, len(a) + 1):
        for j in range(1, len(b) + 1):
            if a[i - 1] == b[j - 1]:
                dp[i][j] = dp[i - 1][j - 1] + 1
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
    return dp[len(a)][len(b)]

print(lcs_length('no', 'on'))

The table has (m + 1)(n + 1) cells at O(1) each: O(mn) time, for edit distance too. Each row reads only itself and the row above, so for the number alone two rows are enough: O(n) memory. The walk back can pass through any row, so recovering the letters or edits keeps the whole table. (Hirschberg's algorithm avoids that, at about twice the work.)

The word subsequence matters: DYNAMIC and PANORAMIC share NAMIC, five letters, as a subsequence, but only AMIC, four, as an unbroken run.

Edit distance has a tempting shortcut too: count the positions where the letters differ.

One sequence can be compared with itself, too. A longest increasing subsequence (LIS) keeps values that strictly rise from left to right. Whether v[i] can extend a run depends on the value the run ends with, so dp[i] is the length of the longest increasing run that ends exactly at v[i]: 1 more than the largest dp[j] over earlier positions j with a smaller value, or 1 if there is none. Each position looks back at every earlier one: n(n − 1)/2 comparisons, O(n²).

dp comes out 1, 2, 2, 3, 3, 1, 2. At the 9, the runs ending at 8 and at 4 tie at length 2, and the first found, 8, is kept. The largest entry, 3, comes first at dp[3], and prev leads back through 8 to 3: the answer is 3, 8, 9, though the last entry, dp[6], is only 2. 21 comparisons.

A faster version keeps tails[k], the smallest value so far that ends an increasing run of length k. A smaller end leaves more room for later values, and tails always rises, so each value finds its place by binary search: it replaces the first tail at least as large as itself or, if there is none, extends the longest run. That is O(n log n).

The same seven values, placed by binary search: 4 replaces 8, 6 replaces 9, then 1 and 2 replace 3 and 4, so tails ends as 1, 2, 6. Its length, 3, is the answer, but 1, 2, 6 is not a subsequence (the 6 comes first); prev recovers 3, 4, 6. 10 comparisons, against 21.

06Check yourself

Check yourself

  1. Question 1 of 6

    What is the longest common subsequence of “baker” and “break”?

    Your answer comes back in Review in a few days, so it sticks.

  2. Question 2 of 6

    In an edit distance table, A[i] ≠ B[j], and the cells above, to the left and diagonally up-left hold 4, 2 and 3. What is D[i,j], and which edit does it end with?

  3. Question 3 of 6

    What is the edit distance from “kayak” to “yak”?

  4. Question 4 of 6

    How long is the longest increasing subsequence of 6, 2, 8, 3, 9, 1?

  5. Question 5 of 6

    A spell checker needs only the edit distance to each dictionary word, not the edits. Words have at most 20 letters. About how much must it keep at once while filling one table?

  6. Question 6 of 6

    In the faster LIS, tails is 2, 5, 9 and the next value is 6. What happens?

07Explain it back

Say it in your own words

How does the edit distance table find the fewest edits that turn one word into another? Say what a cell means, how it is filled, and how you get the edits themselves.

08Recap

What to remember

  • Cell (i, j) answers the question for the first i items of one sequence and the first j of the other. Row 0 and column 0 are the empty prefixes, and the answer is the corner.
  • LCS: a match adds 1 to the diagonal; a mismatch keeps the larger of above and left. Edit distance: the edges count up from 0, and a cell is the smallest of above + 1, left + 1 and diagonal + 1, or the diagonal when the letters match.
  • Both take O(mn) time. Two rows give the number; listing the letters or edits walks back from the corner through the whole table.
  • A subsequence may skip letters, so the LCS is often longer than any shared run. Inserts and deletes shift letters, so edit distance can be far below the count of positions that differ.
  • LIS: dp[i] is the longest increasing run ending exactly at v[i], O(n²), and the answer is the largest entry, not the last. Binary searching the smallest tail of each length makes it O(n log n).
  • Next: practise with Longest Common Subsequence, Fewest Edits Between Two Words and Longest Increasing Subsequence, or try your own input on the LCS, edit distance and LIS visualisers.
Back to path