Longest Common Subsequence
Find the longest subsequence shared by two sequences using a 2D table over prefix pairs.
Overview
Given strings a (length n) and b (length m), the LCS is the longest sequence that is a subsequence of both. For a = "abcde", b = "ace", the LCS is "ace" (length 3). It underlies diff, DNA alignment, and the "minimum insertions/deletions to transform" family; Edit Distance is its close cousin with substitutions.
The DP compares prefixes: dp[i][j] answers the question for a[:i] and b[:j]. When the last characters match they belong to the LCS; otherwise one of them is dropped and the better of the two options is kept.
Intuition
A mental model before the formal terms.
Compare "abcde" and "ace" from the end. e == e, so an optimal answer includes that e and reduces to LCS of "abcd" and "ac". Now d != c: either d is useless (try "abc" vs "ac") or c is useless (try "abcd" vs "a"). Taking the better of those and continuing gives "ac" + "e" = 3.
Lay out a grid with a along the rows and b along the columns. Each cell holds the LCS length of the two prefixes. A match lets you step diagonally up-left and add one; a mismatch lets you take the larger of the cell above and the cell to the left. The bottom-right cell is the answer and the diagonal steps on a walk back are the LCS characters.
How it works
- State:
dp[i][j]= LCS length ofa[0..i)andb[0..j). Table size(n+1) × (m+1). - Transition: if
a[i-1] == b[j-1]:dp[i][j] = dp[i-1][j-1] + 1; elsedp[i][j] = max(dp[i-1][j], dp[i][j-1]). - Base case:
dp[0][j] = dp[i][0] = 0— an empty prefix shares nothing. - Iteration order: row-major,
ifrom 1 ton,jfrom 1 tom; each cell needs its upper, left, and upper-left neighbors, all already computed. - Answer location:
dp[n][m]. Reconstruct by walking from(n, m): on a match move diagonally and record the character; otherwise move toward the larger neighbor. - Space optimization: each row depends only on the previous row, so two rows of length
m+1suffice (O(min(n, m))by putting the shorter string on the columns). Reconstruction, however, needs the full table or Hirschberg's divide-and-conquer trick.
Why it works
Optimal substructure: let Z be an LCS of a[0..i) and b[0..j). If a[i-1] == b[j-1], some LCS ends with that character (if Z does not, append it — it stays common and gets longer, contradiction — or swap its last char for it), so Z minus its last char is an LCS of the two shorter prefixes. If the last characters differ, Z cannot end with both, so it is an LCS of a[0..i-1),b[0..j) or of a[0..i),b[0..j-1). The transition covers precisely these cases.
The number of distinct (i, j) states is (n+1)(m+1) and each takes O(1), giving O(nm) — versus 2^n subsequences in brute force.
Correctness of the two-row optimization follows from the dependency structure: cell (i, j) never reads row i-2 or earlier.
Recognition
How to tell a problem wants this.
- Two strings/sequences and a question about what they share, or the fewest edits limited to insert/delete to make them equal.
- "Subsequence" (not substring) — order matters, contiguity does not.
- Lengths up to a few thousand each — an
n × mtable of ~10⁷ cells fits.
Interactive visualization
Play, step, change the input. ← → and space work too.
| B | D | C | A | B | A | ||
|---|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | |
| A | 0 | · | · | · | · | · | · |
| B | 0 | · | · | · | · | · | · |
| C | 0 | · | · | · | · | · | · |
| B | 0 | · | · | · | · | · | · |
| D | 0 | · | · | · | · | · | · |
| A | 0 | · | · | · | · | · | · |
| B | 0 | · | · | · | · | · | · |
1dp[0][*] = dp[*][0] = 02for i in 1 .. m: for j in 1 .. n:3 if X[i] == Y[j]: dp[i][j] = dp[i-1][j-1] + 14 else: dp[i][j] = max(dp[i-1][j], dp[i][j-1])5traceback from dp[m][n]Pseudocode
1dp = (n+1) x (m+1) table of 02for i in 1..n:3 for j in 1..m:4 if a[i-1] == b[j-1]: dp[i][j] = dp[i-1][j-1] + 15 else: dp[i][j] = max(dp[i-1][j], dp[i][j-1])6return dp[n][m]Implementations
1# dp[i][j] = LCS length of the prefixes a[0..i) and b[0..j).2# Returns one actual LCS; its length is dp[n][m].3def longest_common_subsequence(a: str, b: str) -> str:4 n, m = len(a), len(b)51 · State table6 # Row 0 and column 0 stay 0: an empty prefix shares nothing.7 dp = [[0] * (m + 1) for _ in range(n + 1)]82 · Transition9 for i in range(1, n + 1):10 for j in range(1, m + 1):11 if a[i - 1] == b[j - 1]:12 dp[i][j] = dp[i - 1][j - 1] + 113 else:14 dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])153 · Answer16 length = dp[n][m] # LCS length lives in the last cell174 · Reconstruction18 chars: list[str] = []19 i, j = n, m20 while i > 0 and j > 0:21 if a[i - 1] == b[j - 1]:22 chars.append(a[i - 1]) # match: part of the LCS, move diagonally23 i -= 124 j -= 125 elif dp[i - 1][j] >= dp[i][j - 1]:26 i -= 1 # move toward the larger neighbor27 else:28 j -= 129 assert len(chars) == length30 return "".join(reversed(chars))- The table is a list-comprehension of fresh rows —
[[0] * (m + 1) for _ in range(n + 1)]; the inner[0] * (m + 1)is safe because ints are immutable. - Prefix convention:
dp[i][j]coversa[:i]andb[:j], so the characters compared area[i - 1]andb[j - 1]. - The walk-back appends matched characters in reverse order;
"".join(reversed(chars))restores left-to-right order in one pass. - The
assertdocuments the invariant that reconstruction recovers exactlydp[n][m]characters.
Length-only: two rows → O(min(n, m)) with the shorter string on columns. reversed() is a lazy iterator — no extra copy beyond the final join.
- Never write
[[0] * (m + 1)] * (n + 1)— the outer*copies the ROW REFERENCE n + 1 times and every row aliases the same list. - Appending to a list then joining is the idiomatic O(n) string build;
s = ch + sin a loop is quadratic. - For pure length on large inputs,
difflib.SequenceMatchersolves related matching problems, but its ratio is not the LCS length — implement the table.
- The
[[0] * m] * nrow-aliasing bug — writes show up in every row. - Using
a[i]/b[j]instead of the- 1offsets that the size-(n+1) table requires. - Recursing without memoization — 2^n blowup and the recursion limit long before that.
- 2D-table construction is the trap outside C++: Python
[[0]*m]*nand JS/TSfill(new Array(...))both alias one row; C++std::vectorvalue-initialization builds independent rows by construction. - String building during reconstruction: C++ can write into a pre-sized
std::string; JS/TS/Python strings are immutable — collect characters in an array/list and join once. - Cell values are small ints, so overflow is a non-issue in every language; memory is the binding constraint (C++ flat vectors and JS typed arrays shrink it, Python lists of ints are the heaviest).
Complexity
Full table O(n·m) space is required for straightforward reconstruction; Hirschberg recovers the sequence in O(min(n, m)) space.
Compare growth rates in the Complexity Explorer →When to use — and when not to
- Similarity between two sequences where only insertions and deletions are allowed.
- Diff tools, merge conflict detection, plagiarism and DNA similarity.
- Derived problems: shortest common supersequence (
n + m - LCS), minimum deletions to make strings equal, longest palindromic subsequence (LCS ofsandreverse(s)).
- Longest common *substring* — needs the variant that resets to 0 on mismatch, or suffix automata for large inputs.
- Both strings are
10^5+ long —O(nm)is too slow; use bit-parallel LCS or specialized algorithms (Hunt–Szymanski) when matches are sparse. - One sequence only (Longest Increasing Subsequence) — a different, 1D problem.
Alternatives
Common mistakes
- Indexing off by one between
dp[i](prefix lengthi) anda[i-1](the i-th character). - Adding 1 on a match but also taking the max with the non-diagonal neighbors — harmless for length but breaks reconstruction logic.
- Attempting reconstruction from the two-row version, which has discarded the history.
- Confusing subsequence with substring and resetting to 0 on mismatch.
Interview patterns
- Longest Common Subsequence of two strings.
- Longest Palindromic Subsequence via LCS with the reversed string.
- Delete Operation for Two Strings:
n + m - 2·LCS. - Uncrossed Lines / Max Dot Product of Two Subsequences — LCS-shaped tables.
- Coin ChangeIntermediate