Longest Common Subsequence
Given two strings, find the length of their longest common subsequence: characters that appear in both strings in the same relative order, though not necessarily next to each other.
Do this lesson first: longest common subsequenceExample input
A = "ABCBDAB", B = "BDCABA"
Expected output
4
Break it down
Answer each question out loud before you open it. Getting it wrong here is the useful part. A revealed answer you never guessed at teaches you nothing.
Fill the table
The table pauses before each cell you have to supply. Type the value the recurrence gives, and the write animation confirms it.
Press start. The animation stops at every cell YOUR recurrence must fill.
Row 0 and column 0 are given as 0, because an empty prefix shares nothing with any prefix of the other string. At the drive size that is 14 bases: 7 across row 0 for columns 0 to 6, and 7 more down column 0 for rows 1 to 7. Every interior cell reads only cells above it, to its left, or diagonally above-left, so a row-major sweep, left to right within each row, has finished all three before any of them is needed. The diagonal costs nothing extra: the sweep completed the whole previous row before it started this one.
Cols →
Rows ↓A: ABCBDAB
Cols →B: the first n letters of BDCABADCBA
- 1
function lcs(a, b) { - 2
const m = a.length, n = b.length; - 3
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0)); - 4
for (let i = 1; i <= m; i++) { - 5
for (let j = 1; j <= n; j++) { - 6
const match = a[i - 1] === b[j - 1]; - 7
dp[i][j] = match - 8
? dp[i - 1][j - 1] + 1 - 9
: Math.max(dp[i - 1][j], dp[i][j - 1]); - 10
} - 11
} - 12
return dp[m][n]; - 13
}
The code, the trap, the variations
- 1
function lcs(a, b) { - 2
const m = a.length, n = b.length; - 3
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0)); - 4
for (let i = 1; i <= m; i++) { - 5
for (let j = 1; j <= n; j++) { - 6
const match = a[i - 1] === b[j - 1]; - 7
dp[i][j] = match - 8
? dp[i - 1][j - 1] + 1 - 9
: Math.max(dp[i - 1][j], dp[i][j - 1]); - 10
} - 11
} - 12
return dp[m][n]; - 13
}
Where people go wrong
Adding 1 to the wrong cell on a match. The match branch reads the DIAGONAL, dp[i - 1][j - 1], because pairing the two letters uses both of them up. Writing max(dp[i - 1][j], dp[i][j - 1]) + 1 instead looks like a harmless generalisation, and on this pair it first goes wrong at dp[1][6]. That cell pairs A's first letter, an A, with B's sixth letter, also an A. Its left neighbour dp[1][5] = 1 already spends that same A, matched against B's fourth letter, so max(0, 1) + 1 writes 2 and claims a two-letter common subsequence from a one-letter prefix of A. The diagonal dp[0][5] = 0 is the honest starting point and gives 1. The error compounds from there: the wrong rule finishes this table at 5 instead of 4.
Print one longest common subsequence, not just its length.
The table and its recurrence do not change; a second pass walks back through the finished table from dp[m][n]. On a match, step diagonally and keep the letter; on a mismatch, step toward the larger neighbour; stop at row 0 or column 0 and read the kept letters in reverse. The walk is at most m + n steps and computes no new cell. Its one real choice is a tie, and the tie decides which answer you print: on this pair, the walk from dp[7][6] spells BCBA when ties step up and BDAB when ties step left. The simple walk needs the whole table, so the two-rolling-row saving from the execution panel does not carry over.
Find the length of the shortest common supersequence: the shortest string that contains both A and B as subsequences.
Same table, same recurrence, one subtraction at the end: |A| + |B| - LCS, because every letter of the common subsequence is written once instead of twice and every other letter has to appear. On this pair that is 7 + 6 - 4 = 9. Building the string itself is the walk-back from the variation above, writing the shared letter once on a match, the dropped letter on each mismatch step, and whatever prefix remains when the walk reaches an edge.
Count the fewest single-letter insertions and deletions that turn A into B.
Still the same table: keep the common subsequence, delete the rest of A, insert the rest of B, so the count is |A| + |B| - 2 × LCS, which is 7 + 6 - 8 = 5 on this pair. Allow a third operation, replacing one letter with another, and that shortcut breaks, because one replacement can stand in for a deletion plus an insertion. The recurrence changes shape: a mismatch costs 1 plus the smallest of three neighbours, the diagonal now standing for a replacement, and the table counts edits to minimize rather than shared letters to maximize. The edges change too, from zeros to dp[i][0] = i and dp[0][j] = j, because turning a prefix into the empty string, or the empty string into a prefix, takes one edit per letter. That is the edit-distance page.