Longest Common Subsequence
Every time git shows you a diff, this grid ran first. The longest common subsequence (LCS) of two strings is the longest set of characters that appears in both, in the same order - gaps allowed. Find it, and everything NOT in it becomes the red and green lines of the diff. One simple table, filled cell by cell, sits behind diff tools, DNA alignment, and plagiarism checkers.
Watch the diff grid fill
Press play - or drag the timeline and step through it yourself.| ∅ | R | E | A | C | T | |
|---|---|---|---|---|---|---|
| ∅ | · | · | · | · | · | · |
| T | · | · | · | · | · | · |
| R | · | · | · | · | · | · |
| A | · | · | · | · | · | · |
| C | · | · | · | · | · | · |
| E | · | · | · | · | · | · |
Two strings: "TRACE" down the side, "REACT" across the top. Each cell will answer: how long is the LCS of these two prefixes?
Complexity
Two drafts of the same essay
Your teacher has your first draft and your final draft, and wants to see what changed. The smart move is to first find what stayed the same: the longest run of sentences that appears in both drafts, in the same order - even with new sentences squeezed in between them. That shared thread is the longest common subsequence. Every sentence on it is 'unchanged'. Every sentence only in the old draft was deleted; every sentence only in the new one was added. This is exactly what git diff does with the lines of your code.
How it works, step by step
Write one string down the side and the other across the top. Add an extra ∅ row and column: against an empty prefix, the answer is 0.
Each cell asks one small question: how long is the LCS of the row-prefix and the column-prefix that end here?
If the two letters match, take the diagonal neighbour and add 1 - this match extends the best answer that ignored both letters.
If they differ, copy the bigger of the cell above and the cell to the left - drop the last letter of one string or the other, and keep the better result.
Fill the table row by row. The bottom-right cell holds the LCS length.
For the string itself, walk back from that corner: every diagonal step is a matched letter. Read them in reverse.
The code, in JavaScript
The full grid, exactly as the animation fills it. dp[i][j] answers a smaller version of the question; the bottom-right cell answers the real one.
function lcsLength(a, b) {
const n = a.length, m = b.length;
// dp[i][j] = LCS length of a's first i letters and b's first j letters.
// Row 0 and column 0 stay 0: against an empty string, nothing is shared.
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
for (let i = 1; i <= n; i++) {
for (let j = 1; j <= m; j++) {
if (a[i - 1] === b[j - 1]) {
// Match: extend the best answer that used neither letter.
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
// No match: drop the last letter of one string or the other.
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[n][m]; // bottom-right corner = the answer
}
lcsLength("REACT", "TRACE"); // → 3 (they share "RAC")
lcsLength("SUNDAY", "SATURDAY"); // → 5 (they share "SUDAY")Dry run: the LCS of "REACT" and "TRACE"
The same steps the visualizer plays, written as a table. Generated by running the real algorithm - not written by hand.
| Step | Row | What happened |
|---|---|---|
| 1 | 1 | T vs R - different, take the best neighbour: max(0, 0) = 0. |
| 2 | 1 | T vs E - different, take the best neighbour: max(0, 0) = 0. |
| 3 | 1 | T vs A - different, take the best neighbour: max(0, 0) = 0. |
| 4 | 1 | T vs C - different, take the best neighbour: max(0, 0) = 0. |
| 5 | 1 | T vs T - match! diagonal + 1 = 1. |
| 6 | 2 | R vs R - match! diagonal + 1 = 1. |
| 7 | 3 | A vs A - match! diagonal + 1 = 2. |
| 8 | 4 | C vs C - match! diagonal + 1 = 3. |
| 9 | 5 | E vs E - match! diagonal + 1 = 2. |
| 10 | 5 | E vs T - no match: up and left tie at 3, follow up. |
| 11 | 4 | C vs T - no match: follow the bigger neighbour (left, 3). |
| 12 | 4 | C vs C - a match: C is part of the LCS, step diagonally. |
| 13 | 3 | A vs A - a match: A is part of the LCS, step diagonally. |
| 14 | 2 | R vs E - no match: follow the bigger neighbour (left, 1). |
| 15 | 2 | R vs R - a match: R is part of the LCS, step diagonally. |
Good choice when…
- You are diffing two versions of anything - files, configs, lists. Lines on the LCS are 'unchanged'; everything else becomes the + and - lines.
- Sequence similarity where order matters but gaps are fine: DNA and protein alignment, plagiarism detection, matching noisy logs.
- You need a similarity score between two sequences: LCS length divided by the longer length is a simple, order-aware measure.
- As the template for a whole DP family: edit distance, shortest common supersequence, and 'minimum deletions to make a palindrome' are all this grid with small twists.
Bad choice when…
- You need the longest common substring (one contiguous block) - that is a different recurrence (reset to 0 on mismatch), and suffix structures often do it better.
- The inputs are huge: two 100,000-line files mean 10 billion cells. Real diff tools use Myers' algorithm, which is fast when the files are mostly similar.
- Order does not matter - then you only want the common *set* of items, and a hash set answers that in O(n + m).
Common mistakes
- Subsequence is not substring: gaps are allowed. "RAC" is not a substring of "REACT" (the E sits in the middle), yet it is a perfectly valid subsequence. Solving the wrong one of the two is a classic interview miss.
- The length is unique, the string is not: when up and left tie, both choices are valid but lead to different subsequences. `>=` vs `>` in the traceback silently changes which answer you return - decide deliberately if the caller cares.
- The two-row memory trick keeps the length but destroys the traceback - you cannot recover the string from two rows. Hirschberg's divide-and-conquer gets both, O(m) memory *and* the string, at the cost of trickier code.
- O(n·m) memory blows up fast: two 100k-character documents want 10 billion cells. Production diffs (git included) use Myers' O(n·d) algorithm, which only does work proportional to how *different* the files are.
Longest Common Subsequence vs. its closest relatives
| Algorithm | Best | Average | Worst | Space | Stable |
|---|---|---|---|---|---|
| Longest Common Subsequencethis page | O(n·m) | O(n·m) | O(n·m) | O(n·m) | - |
| 0/1 Knapsack | O(n·W) | O(n·W) | O(n·W) | O(W) | - |
| Coin Change | O(n·a) | O(n·a) | O(n·a) | O(a) | - |
| KMP Pattern Matching | O(n + m) | O(n + m) | O(n + m) | O(m) | - |