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 nn has 2n2^n 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 O(mn)O(mn)-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.

Key takeaways
  • LCS asks for the longest subsequence common to two strings, preserving relative order but allowing gaps.
  • The key recurrence compares prefixes of length ii and jj: 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 O(mn)O(mn) 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 O(min⁡(m,n))O(\min(m,n)) 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 core insight

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 O(mn)O(mn) time.

The idea

A subsequence of a string X=x1x2…xmX = x_1 x_2 \dots x_m is any sequence obtainable by deleting zero or more characters from XX 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 XX (length mm) and YY (length nn), the Longest Common Subsequence problem asks for a longest string ZZ that is a subsequence of both XX and YY. Note carefully: ZZ 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 Z=z1…zkZ = z_1 \dots z_k is an LCS of XX and YY, then:

  • If xm=ynx_m = y_n, then zk=xm=ynz_k = x_m = y_n, and z1…zk−1z_1 \dots z_{k-1} is an LCS of Xm−1=x1…xm−1X_{m-1} = x_1\dots x_{m-1} and Yn−1=y1…yn−1Y_{n-1} = y_1 \dots y_{n-1}.
  • If xm≠ynx_m \neq y_n, then ZZ is a common subsequence of Xm−1,YX_{m-1}, Y or of X,Yn−1X, Y_{n-1} (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 (i,j)=(3,5)(i,j) = (3,5) is reused by many different recursive paths from (m,n)(m,n). 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 c[i][j]c[i][j] as the length of an LCS of the prefixes Xi=x1…xiX_i = x_1 \dots x_i and Yj=y1…yjY_j = y_1 \dots y_j, for 0≤i≤m0 \le i \le m, 0≤j≤n0 \le j \le n. The base cases are c[0][j]=0c[0][j] = 0 for all jj and c[i][0]=0c[i][0] = 0 for all ii, since an empty prefix has no common subsequence with anything except the empty string. The recurrence formalizes the substructure argument above:

c[i][j]={0if i=0 or j=0c[i−1][j−1]+1if i,j>0 and xi=yjmax⁡(c[i−1][j], c[i][j−1])if i,j>0 and xi≠yjc[i][j] = \begin{cases} 0 & \text{if } i = 0 \text{ or } j = 0 \\ c[i-1][j-1] + 1 & \text{if } i, j > 0 \text{ and } x_i = y_j \\ \max\big(c[i-1][j],\ c[i][j-1]\big) & \text{if } i, j > 0 \text{ and } x_i \neq y_j \end{cases}

Tabulation fills this table in increasing order of ii and jj, so that by the time we need c[i−1][j−1]c[i-1][j-1], c[i−1][j]c[i-1][j], and c[i][j−1]c[i][j-1], all three are already computed. The answer to the whole problem is c[m][n]c[m][n]. 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 (m,n)(m, n) to (0,0)(0, 0): 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.

Figure
Filling the DP table for X = "ABCBDAB", Y = "BDCAB": each cell depends on its left, up, and upper-left neighbors
dp[i][j] built from dp[i-1][j-1], dp[i-1][j], dp[i][j-1] ε B D C A B ε A B C B D A B c[i-1][j-1] c[i-1][j] c[i][j-1] c[i][j]=✓ X[i]='A' equals Y[j]='A' ⇒ diagonal move, c[i][j] = c[i-1][j-1] + 1 Backtracking follows these same diagonal arrows to reconstruct the LCS string.

Complexity & correctness

Time complexity. The nested loop structure visits each of the (m+1)(n+1)(m+1)(n+1) cells exactly once, and each cell does O(1)O(1) work — one comparison and one constant-time recurrence evaluation. Hence

T(m,n)=Θ(mn).T(m,n) = \Theta(mn).

This is exponentially better than the brute-force O(2m⋅n)O(2^m \cdot n) approach of enumerating subsequences of XX and checking each against YY. Backtracking to reconstruct the string takes O(m+n)O(m+n) time, since each step of the walk decreases ii, jj, or both by one, so the path length is bounded by m+nm+n.

Space complexity. Storing the full tables cc and bb costs Θ(mn)\Theta(mn) space. If only the length of the LCS is required, not the subsequence itself, this can be compressed to O(min⁡(m,n))O(\min(m,n)) by keeping only two rows (or columns) of cc at a time, since row ii depends only on row i−1i-1 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 O(m+n)O(m+n) space with full reconstruction, at roughly double the time constant) or falling back to the full O(mn)O(mn) table.

Correctness. The proof is a direct induction on i+ji + j, mirroring the optimal-substructure argument. Base case: c[i][0]=c[0][j]=0c[i][0] = c[0][j] = 0 for all i,ji, j, since the empty string shares no nonempty subsequence with anything. Inductive step: assume c[i′][j′]c[i'][j'] is correctly the LCS length of Xi′X_{i'}, Yj′Y_{j'} for all i′+j′<i+ji' + j' < i+j. Consider c[i][j]c[i][j].

  • If xi=yjx_i = y_j: any common subsequence of Xi,YjX_i, Y_j that does not end by matching xix_i with yjy_j can be shown to be no longer than one that does (a standard exchange argument — if an LCS ZZ 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 Xi,YjX_i, Y_j is exactly the LCS of Xi−1,Yj−1X_{i-1}, Y_{j-1} extended by this shared character, giving c[i][j]=c[i−1][j−1]+1c[i][j] = c[i-1][j-1] + 1.
  • If xi≠yjx_i \neq y_j: an LCS of Xi,YjX_i, Y_j cannot use both xix_i and yjy_j as its final aligned pair (they are unequal), so it either ignores xix_i (giving a common subsequence of Xi−1,YjX_{i-1}, Y_j) or ignores yjy_j (giving one of Xi,Yj−1X_i, Y_{j-1}) — 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 xix_i" and "the unused yjy_j" while both remain unused. Taking the better of the two smaller subproblems, c[i][j]=max⁡(c[i−1][j],c[i][j−1])c[i][j] = \max(c[i-1][j], c[i][j-1]), is therefore correct and cannot overcount.

By induction, c[m][n]c[m][n] is exactly the LCS length of XX and YY. The backtracking procedure follows recorded directions that correspond precisely to the case applied at each cell, so it reconstructs a string of length c[m][n]c[m][n] that is by construction a common subsequence — establishing both feasibility and optimality.

Honest tradeoffs. LCS's O(mn)O(mn) 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 O(mn)O(mn) table is the right tool, and its simplicity is a genuine advantage over cleverer but fragile alternatives.

Worked example

Let X=ABCBDABX = \texttt{ABCBDAB} (m=7m=7) and Y=BDCABY = \texttt{BDCAB} (n=5n=5). We build c[i][j]c[i][j] for i=0,…,7i=0,\dots,7 and j=0,…,5j=0,\dots,5. Row/column 00 are all zero. Filling row by row (showing the interesting rows):

i\ji \backslash j0 (ε)1 (B)2 (D)3 (C)4 (A)5 (B)
0 (ε)000000
1 (A)000011
2 (B)011112
3 (C)011222
4 (B)011223
5 (D)012223
6 (A)012233
7 (B)012234

Trace the two cells that most reward attention:

  • c[3][3]c[3][3]: X[3]=CX[3] = \texttt{C}, Y[3]=CY[3] = \texttt{C} — they match, so c[3][3]=c[2][2]+1=1+1=2c[3][3] = c[2][2] + 1 = 1 + 1 = 2.
  • c[6][4]c[6][4]: X[6]=AX[6] = \texttt{A}, Y[4]=AY[4] = \texttt{A} — match, so c[6][4]=c[5][3]+1=2+1=3c[6][4] = c[5][3] + 1 = 2 + 1 = 3.
  • c[2][1]c[2][1]: X[2]=BX[2]=\texttt{B}, Y[1]=BY[1]=\texttt{B} — match, c[2][1]=c[1][0]+1=0+1=1c[2][1] = c[1][0]+1 = 0+1=1.
  • c[5][1]c[5][1]: X[5]=DX[5]=\texttt{D}, Y[1]=BY[1]=\texttt{B} — mismatch, c[5][1]=max⁡(c[4][1],c[5][0])=max⁡(1,0)=1c[5][1] = \max(c[4][1], c[5][0]) = \max(1,0) = 1.

The final answer, c[7][5]=4c[7][5] = 4, is the LCS length.

Backtracking from (7,5)(7,5): 1. (7,5)(7,5): X[7]=BX[7]=\texttt{B}, Y[5]=BY[5]=\texttt{B} — match. Emit `B`. Move to (6,4)(6,4). 2. (6,4)(6,4): X[6]=AX[6]=\texttt{A}, Y[4]=AY[4]=\texttt{A} — match. Emit `A`. Move to (5,3)(5,3). 3. (5,3)(5,3): X[5]=DX[5]=\texttt{D}, Y[3]=CY[3]=\texttt{C} — mismatch. c[4][3]=2c[4][3]=2, c[5][2]=2c[5][2]=2 — tie, take UP (a valid tie-break). Move to (4,3)(4,3). 4. (4,3)(4,3): X[4]=BX[4]=\texttt{B}, Y[3]=CY[3]=\texttt{C} — mismatch. c[3][3]=2≥c[4][2]=1c[3][3]=2 \ge c[4][2]=1, move UP to (3,3)(3,3). 5. (3,3)(3,3): X[3]=CX[3]=\texttt{C}, Y[3]=CY[3]=\texttt{C} — match. Emit `C`. Move to (2,2)(2,2). 6. (2,2)(2,2): X[2]=BX[2]=\texttt{B}, Y[2]=DY[2]=\texttt{D} — mismatch. c[1][2]=0c[1][2]=0, c[2][1]=1c[2][1]=1 — take LEFT, move to (2,1)(2,1). 7. (2,1)(2,1): X[2]=BX[2]=\texttt{B}, Y[1]=BY[1]=\texttt{B} — match. Emit `B`. Move to (1,0)(1,0). 8. i=1,j=0i=1, j=0: 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

PitfallWhy it happensFix
Confusing LCS with longest common substringSubstring must be contiguous; subsequence allows gapsRe-read the problem statement; substring uses a different recurrence that resets to 0 on mismatch
Off-by-one indexingStrings are 1-indexed in the recurrence but 0-indexed in most languagesKeep `dp` sized `(m+1) x (n+1)` with row/col 0 as sentinels for the empty prefix
Forgetting to reverse the backtracked stringReconstruction walks from `(m,n)` back to `(0,0)`, so characters emerge in reverse orderAlways reverse (or prepend instead of append) before returning
Wrong tie-break silently changing which LCS is returnedMultiple distinct LCS's of the same optimal length can existUnderstand that length is always correct; the specific string returned depends on tie-breaking and is not unique
Assuming O(mn)O(mn) can be beaten in generalThe SETH-based conditional lower bound is easy to overlookTreat sub-quadratic LCS as an open problem for general alphabets, not a bug in your code
Using full O(mn)O(mn) space when only the length is neededCopy-pasting the reconstruction version even when unnecessaryUse 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
RE
Richard E. Bellman
1920–1984 · American applied mathematician who founded dynamic programming while at RAND Corporation, formalizing the principle of optimality that underlies LCS, edit distance, and shortest-path recurrences

History
1950
s — Richard Bellman formalizes dynamic programming and the principle of optimality at RAND.
1970
s — Sequence-comparison algorithms (Needleman–Wunsch, Wagner–Fischer) formalize alignment recurrences closely related to LCS.
1975–1977
— Hunt and Szymanski publish faster LCS algorithms exploiting sparse match structure, motivated directly by file-comparison tools.
1970
s–1980s — Unix `diff`, built on LCS-style alignment, becomes a standard tool for source-code version control.
2010
s — Conditional lower bounds (via the Strong Exponential Time Hypothesis) show that beating O(mn)O(mn) time for general LCS would refute a major complexity conjecture.

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 (i,j)(i,j)-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 c[m][n]c[m][n] 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 (m+1)×(n+1)(m+1)\times(n+1) 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 m+n−2⋅LCS(X,Y)m + n - 2 \cdot \text{LCS}(X,Y), 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 O(mn)O(mn) 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 (i,j)(i,j) pair is visited during any computation of c[m][n]c[m][n], 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 O((mn)1−ϵ)O((mn)^{1-\epsilon}) time for some fixed ϵ>0\epsilon > 0) 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 XiX_i as "the first ii 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.

Before this

Where this leads