Longest Common Subsequence
Imagine comparing two versions of a document, two DNA strands, or two commit histories in `git diff`. You might ask: what is the largest amount of shared structure between them? This must allow for insertions and deletions in between. This is precisely the Longest Common Subsequence (LCS) problem. It asks us to find the longest sequence of characters that appears in both strings. The characters must appear in the same relative order, but not necessarily contiguously. It is the single most instructive example of two-dimensional dynamic programming. It quietly powers version-control diff tools, bioinformatics sequence alignment, spell-checkers, and plagiarism detectors. The naive approach — try every subsequence of one string and check whether it appears in the other — is hopelessly exponential. A string of length has subsequences. Yet the problem has a beautiful recursive structure. The LCS of two strings can always be expressed in terms of the LCS of their prefixes. This "optimal substructure" property, combined with heavy overlap between subproblems, is exactly the signature dynamic programming exploits. It turns an exponential search into an -time table-filling procedure. This article builds LCS from first principles. It covers the recurrence on prefixes, the two-dimensional table that tabulates it bottom-up. It also covers the backtracking walk that recovers not just the length of the LCS. That walk recovers an actual optimal subsequence too. We connect this to the general DP paradigm introduced with memoization versus tabulation. We show concretely how a recursive definition on `(i, j)` prefix pairs becomes a filled grid. By the end you will be able to derive the recurrence yourself. You will trace the DP table by hand on a real example. You will reconstruct the LCS string from the table. You will also see precisely why this same grid-of-prefixes idea generalizes almost verbatim. It extends into Edit Distance — one of the most consequential ideas in string algorithms.
- LCS asks for the longest subsequence common to two strings, preserving relative order but allowing gaps.
- The key recurrence compares prefixes of length and : matching last characters extends the LCS by one; mismatches force a choice between two smaller subproblems.
- A 2D table `dp[i][j]` tabulates the LCS length of prefixes `X[1..i]` and `Y[1..j]`, filled in time and space.
- Backtracking through the filled table, from `dp[m][n]` to `dp[0][0]`, reconstructs an actual optimal subsequence, not just its length.
- Space can be compressed to if only the length is needed, at the cost of losing easy reconstruction.
- The same prefix-pair recurrence generalizes directly into edit distance, sequence alignment, and diff algorithms.
The LCS of two strings decomposes exactly into the LCS of their prefixes, so a two-dimensional table of prefix-pair answers can be filled bottom-up in time.
The idea
A subsequence of a string is any sequence obtainable by deleting zero or more characters from without reordering the remaining ones. For example, `ACE` is a subsequence of `ABCDE`, but `AEC` is not, since it violates the original order. Given two strings (length ) and (length ), the Longest Common Subsequence problem asks for a longest string that is a subsequence of both and . Note carefully: need not be contiguous in either string — that distinct, harder problem is the longest common substring, and confusing the two is a classic error. The crucial structural fact is optimal substructure: if is an LCS of and , then:
- If , then , and is an LCS of and .
- If , then is a common subsequence of or of (at least one, possibly both), and it must be the longer of the two best such subsequences.
This is the exact same style of reasoning used in Introduction to Dynamic Programming: a problem on the full input reduces to a problem on strictly smaller sub-inputs, and the sub-inputs overlap enormously — the prefix pair is reused by many different recursive paths from . That overlap is what separates DP from plain divide-and-conquer, and it is what makes memoization versus tabulation a meaningful design choice rather than an afterthought.
How it works
Define as the length of an LCS of the prefixes and , for , . The base cases are for all and for all , since an empty prefix has no common subsequence with anything except the empty string. The recurrence formalizes the substructure argument above:
Tabulation fills this table in increasing order of and , so that by the time we need , , and , all three are already computed. The answer to the whole problem is . To recover the actual subsequence (not merely its length), we record, alongside each cell, which case produced it — a "direction" pointer — and then walk backward from to : whenever a diagonal move was taken (a match), we emit that character; whenever an up or left move was taken, we simply move without emitting anything. Because we walk backward, the emitted characters come out in reverse order and must be reversed once at the end. ``` function LCS-LENGTH(X, Y): m = length(X) n = length(Y) let c[0..m][0..n] be a new table // c[i][j] = LCS length of X[1..i], Y[1..j] let b[0..m][0..n] be a new table // direction: DIAG, UP, or LEFT for i = 0 to m: c[i][0] = 0 for j = 0 to n: c[0][j] = 0 for i = 1 to m: for j = 1 to n: if X[i] == Y[j]: c[i][j] = c[i-1][j-1] + 1 b[i][j] = DIAG else if c[i-1][j] >= c[i][j-1]: c[i][j] = c[i-1][j] b[i][j] = UP else: c[i][j] = c[i][j-1] b[i][j] = LEFT return c[m][n], b
function BUILD-LCS(b, X, i, j): result = empty list while i > 0 and j > 0: if b[i][j] == DIAG: append X[i] to result i = i - 1 j = j - 1 else if b[i][j] == UP: i = i - 1 else: j = j - 1 reverse(result) return result ``` The `LCS-LENGTH` procedure is pure tabulation — bottom-up filling in an order that respects dependencies — versus the equally valid alternative of top-down memoized recursion on `(i, j)` pairs, which computes exactly the same values on demand and caches them. Both realize the identical recurrence; tabulation is typically preferred here because the dependency structure (a full rectangular grid, no missing cells) means we will need essentially every entry anyway, so there is no time saved by laziness, and iteration avoids recursion-stack overhead.
Complexity & correctness
Time complexity. The nested loop structure visits each of the cells exactly once, and each cell does work — one comparison and one constant-time recurrence evaluation. Hence
This is exponentially better than the brute-force approach of enumerating subsequences of and checking each against . Backtracking to reconstruct the string takes time, since each step of the walk decreases , , or both by one, so the path length is bounded by .
Space complexity. Storing the full tables and costs space. If only the length of the LCS is required, not the subsequence itself, this can be compressed to by keeping only two rows (or columns) of at a time, since row depends only on row and itself. This is the same rolling-array trick applicable to many DP recurrences with a narrow dependency band. The tradeoff is real: with only two rows retained, the backtracking pointers are gone, and reconstructing the actual sequence would require either recomputing sub-tables (Hirschberg's algorithm achieves space with full reconstruction, at roughly double the time constant) or falling back to the full table.
Correctness. The proof is a direct induction on , mirroring the optimal-substructure argument. Base case: for all , since the empty string shares no nonempty subsequence with anything. Inductive step: assume is correctly the LCS length of , for all . Consider .
- If : any common subsequence of that does not end by matching with can be shown to be no longer than one that does (a standard exchange argument — if an LCS omits using this final matching pair, we can always extend it or find an equally long alternative that uses it, because the last characters are identical and available in both prefixes). Hence the LCS of is exactly the LCS of extended by this shared character, giving .
- If : an LCS of cannot use both and as its final aligned pair (they are unequal), so it either ignores (giving a common subsequence of ) or ignores (giving one of ) — possibly both are ignorable, but at least one must be, since the LCS's last character is some single symbol that cannot simultaneously be "the unused " and "the unused " while both remain unused. Taking the better of the two smaller subproblems, , is therefore correct and cannot overcount.
By induction, is exactly the LCS length of and . The backtracking procedure follows recorded directions that correspond precisely to the case applied at each cell, so it reconstructs a string of length that is by construction a common subsequence — establishing both feasibility and optimality.
Honest tradeoffs. LCS's time is worst-case tight for the standard DP: no algorithm faster than roughly quadratic time is known for general alphabets under the mild computational assumptions used in fine-grained complexity theory (there is a conditional lower bound tying LCS to the Strong Exponential Time Hypothesis, meaning a truly subquadratic algorithm for all inputs would be a major breakthrough). Faster algorithms exist for special cases: bounded alphabets, few matches, or when the answer is known to be short (e.g., Hunt–Szymanski-style techniques using the number of matching pairs). For most practical purposes — diff tools, bioinformatics — the plain table is the right tool, and its simplicity is a genuine advantage over cleverer but fragile alternatives.
Worked example
Let () and (). We build for and . Row/column are all zero. Filling row by row (showing the interesting rows):
| 0 (ε) | 1 (B) | 2 (D) | 3 (C) | 4 (A) | 5 (B) | |
|---|---|---|---|---|---|---|
| 0 (ε) | 0 | 0 | 0 | 0 | 0 | 0 |
| 1 (A) | 0 | 0 | 0 | 0 | 1 | 1 |
| 2 (B) | 0 | 1 | 1 | 1 | 1 | 2 |
| 3 (C) | 0 | 1 | 1 | 2 | 2 | 2 |
| 4 (B) | 0 | 1 | 1 | 2 | 2 | 3 |
| 5 (D) | 0 | 1 | 2 | 2 | 2 | 3 |
| 6 (A) | 0 | 1 | 2 | 2 | 3 | 3 |
| 7 (B) | 0 | 1 | 2 | 2 | 3 | 4 |
Trace the two cells that most reward attention:
- : , — they match, so .
- : , — match, so .
- : , — match, .
- : , — mismatch, .
The final answer, , is the LCS length.
Backtracking from : 1. : , — match. Emit `B`. Move to . 2. : , — match. Emit `A`. Move to . 3. : , — mismatch. , — tie, take UP (a valid tie-break). Move to . 4. : , — mismatch. , move UP to . 5. : , — match. Emit `C`. Move to . 6. : , — mismatch. , — take LEFT, move to . 7. : , — match. Emit `B`. Move to . 8. : loop stops (`j = 0`). Emitted in reverse pop order `B, A, C, B` → reverse → `BCAB`, a valid LCS of length 4. (Note `BDAB` is not a common subsequence check target here — `BCAB` is confirmed: it is a subsequence of `ABCBDAB` at positions 2,3,4? Let's verify directly: `A-B-C-B-D-A-B`, picking positions 2(B),3(C),6(A),7(B) gives `B,C,A,B` = `BCAB` ✓, and in `Y=BDCAB` picking positions 1(B),3(C),4(A),5(B) gives `B,C,A,B` = `BCAB` ✓.)
Common pitfalls & traps
| Pitfall | Why it happens | Fix |
|---|---|---|
| Confusing LCS with longest common substring | Substring must be contiguous; subsequence allows gaps | Re-read the problem statement; substring uses a different recurrence that resets to 0 on mismatch |
| Off-by-one indexing | Strings are 1-indexed in the recurrence but 0-indexed in most languages | Keep `dp` sized `(m+1) x (n+1)` with row/col 0 as sentinels for the empty prefix |
| Forgetting to reverse the backtracked string | Reconstruction walks from `(m,n)` back to `(0,0)`, so characters emerge in reverse order | Always reverse (or prepend instead of append) before returning |
| Wrong tie-break silently changing which LCS is returned | Multiple distinct LCS's of the same optimal length can exist | Understand that length is always correct; the specific string returned depends on tie-breaking and is not unique |
| Assuming can be beaten in general | The SETH-based conditional lower bound is easy to overlook | Treat sub-quadratic LCS as an open problem for general alphabets, not a bug in your code |
| Using full space when only the length is needed | Copy-pasting the reconstruction version even when unnecessary | Use the two-row rolling array if backtracking is not required |
Pioneers & history
The LCS problem sits at the intersection of combinatorics on words, computational biology, and the broader development of dynamic programming as a paradigm. Dynamic programming itself was formalized by Richard Bellman in the 1950s while working on multistage decision processes at the RAND Corporation; Bellman's "principle of optimality" — that an optimal solution to a problem contains optimal solutions to its subproblems — is exactly the property LCS exploits when reasoning about prefixes. The specific application to comparing biological sequences and text became prominent through the 1970s, driven by the practical need to align DNA and protein sequences and to build efficient file-comparison tools; the Unix `diff` utility and its descendants are direct engineering applications of LCS-style alignment.
Origins · 1 minRichard E. Bellman and how this was discovered
Where this matters
Version-control systems such as Git and Subversion compute file diffs using LCS-style alignment: the "unchanged" lines of two file versions form a longest common subsequence, and everything else is displayed as an insertion or deletion around it. Bioinformatics tools for DNA and protein alignment — descendants of the Needleman–Wunsch and Smith–Waterman algorithms — generalize the exact same prefix-pair recurrence, adding weighted scoring for matches, mismatches, and gaps. Spell-checkers and plagiarism-detection systems use LCS or its close relatives to measure similarity between strings that may differ by small insertions or deletions. Compilers and structural-diff tools for abstract syntax trees use tree-generalized versions of the same idea to produce minimal, human-readable change summaries. The recurrence taught here is also the direct conceptual ancestor of Edit Distance: instead of only "match" or "skip" transitions, edit distance additionally charges a cost for insertion, deletion, and substitution, turning the same -prefix grid into a minimization instead of a maximization. Anyone comfortable with the LCS table has already done nearly all the conceptual work required for edit distance, and more broadly for the family of sequence-alignment dynamic programs used throughout computational biology and natural language processing.
:::quiz[In the LCS recurrence, what happens when X[i] equals Y[j]?]
- c[i][j] = max(c[i-1][j], c[i][j-1])
- c[i][j] = c[i-1][j-1] + 1 ✓
- c[i][j] = 0
- c[i][j] = c[i][j-1] - 1
Ask the tutor: How does the LCS recurrence change if we also want to allow character substitutions with a cost, turning this into the Edit Distance problem?
Is the longest common subsequence unique?
No. Multiple distinct strings can all be valid LCS's of the same maximum length. For example, `X = "AC"` and `Y = "CA"` both have LCS length 1, and either `"A"` or `"C"` is a valid answer — both are correct simultaneously. The DP table always computes the correct length uniquely, since is a single well-defined number, but the backtracking step makes an implicit tie-breaking choice whenever `c[i-1][j] == c[i][j-1]` during a mismatch. Different tie-break rules (favoring UP versus LEFT) can produce different but equally valid optimal subsequences. If you need all LCS's, you must branch at every tie during backtracking, which can be exponential in the worst case since the number of distinct LCS's is not bounded by a polynomial in general.
How does LCS relate to Edit Distance?
Both are computed on the same grid of prefix pairs, and both rely on identical optimal-substructure reasoning. LCS only allows two operations conceptually — "match and extend" or "skip a character from one string" — and it is a maximization over shared length. Edit distance (Levenshtein distance) instead minimizes a cost, and it explicitly charges for insertion, deletion, and substitution as three separate transitions rather than just two. In fact, when substitutions are disallowed or cost infinitely more than an insert+delete pair, edit distance reduces to , since the minimal edit script is exactly "delete everything not in the LCS, then insert everything in Y not in the LCS."
Why is tabulation preferred over memoized recursion for LCS?
Both compute identical values via identical recurrences, and both run in time given a hash map or array cache for memoization. Tabulation is typically favored for LCS specifically because the dependency graph is a dense rectangular grid — essentially every pair is visited during any computation of , so there is no "wasted work" that top-down laziness would avoid. Iterative tabulation also avoids the overhead and stack-depth risk of deep recursion (a recursive solution on strings of length in the thousands can overflow a call stack), and it makes the two-row space optimization for length-only queries far more natural to implement.
Can LCS be solved faster than O(mn) in general?
Not for arbitrary alphabets, under standard complexity assumptions. A conditional lower bound, based on the Strong Exponential Time Hypothesis (SETH), shows that a truly subquadratic algorithm (running in time for some fixed ) for general LCS would imply faster algorithms for Boolean satisfiability than currently believed possible. Faster algorithms do exist in restricted settings: when the alphabet is small and fixed, when the number of matching character pairs is small (Hunt–Szymanski-style methods, useful when comparing mostly-similar files), or when using bit-parallel tricks (Crochemore–Myers–Miller style algorithms exploiting word-level parallelism) that shave constant factors rather than the asymptotic exponent.
What's a common mistake when implementing the base cases?
The most frequent bug is off-by-one errors from mixing 1-indexed recurrence notation with 0-indexed arrays in code. The mathematical recurrence naturally treats as "the first characters," so `c[0][]` and `c[][0]` represent empty prefixes and must be initialized to zero — but in a 0-indexed language, `X[i]` in the recurrence actually corresponds to array index `X[i-1]`. Failing to shift this by one produces subtle mismatches where the algorithm compares the wrong characters, often still returning a plausible-looking but incorrect length. The safest practice is to allocate the table with an explicit extra row and column of sentinels, exactly as shown in the pseudocode, and to always convert `X[i]` in formulas to `X[i-1]` in code deliberately.
Does LCS handle repeated characters correctly?
Yes, the recurrence handles repeats without any special-casing, because it reasons purely about prefix lengths and character equality at specific positions, not about character identity globally. For instance, `X = "AAAA"` and `Y = "AA"` correctly yields an LCS length of 2 (you can only match each `A` in `Y` once per position pair), and the DP table naturally enforces this because advancing `j` consumes a character of `Y` that cannot be reused. A common misconception is worrying that repeated characters create ambiguity in which occurrence matches which — but since we only need the LCS length and one valid witness string, any consistent choice of matching positions is correct, and the recurrence guarantees consistency automatically.
What happens if one of the input strings is empty?
If either string is empty, the LCS is trivially the empty string, with length 0. In the DP table this is exactly the base case: row 0 and column 0 are both entirely zero by definition, corresponding to comparing an empty prefix against any prefix of the other string. No special-case branch is needed in the main loop, which is a strong signal of a well-designed recurrence — the empty-input case falls out naturally from the boundary initialization rather than requiring extra conditional logic. This is a useful sanity check when implementing similar DP recurrences: if the empty-input case requires bespoke handling deep inside the main loop, that is often a hint the recurrence or indexing is set up incorrectly.