Sequence Comparison: LCS, Edit Distance & Diff
A precise guide to longest common subsequences, edit distance, and textual diff: what each objective means, how dynamic programs and edit graphs solve it, how to reconstruct an answer, and where tokenization or cost choices change the result.
Sequence comparison is not one problem. The same two inputs can be compared by asking what they share, how much one must be edited to become the other, or which changes should be shown to a human. Those objectives are related, but they are not interchangeable.
Before choosing an algorithm, define three things:
- the tokens being compared—bytes, Unicode code points, grapheme clusters, words, lines, syntax nodes, or some domain object;
- the allowed operations—insert, delete, substitute, move, transpose, or a domain-specific action;
- the cost and output contract—only a number, one optimal alignment, every optimum, or a readable patch.
Changing any one of these can change both the answer and the suitable algorithm.
Three objectives that are easy to conflate #
Let and be token sequences.
- Longest common subsequence (LCS)
- A longest sequence obtainable from both inputs by deleting zero or more tokens without reordering the survivors. Tokens need not be adjacent.
- Edit distance
- The minimum total cost of an allowed sequence of edits that transforms one input into the other.
- Diff
- A representation of change, usually as retained, inserted, and deleted token runs. A diff tool must choose an edit objective, but it also makes presentation choices such as tokenization, tie-breaking, context, and hunk boundaries.
For ABCBDAB and BDCABA, an LCS has length 4; BCBA and BDAB are two possible answers. Neither is a substring, because a substring must occupy one contiguous interval. LCS solves order-preserving selection, not contiguous matching.
Unit-cost Levenshtein distance allows insertion, deletion, and substitution, each at cost 1. A conventional line diff instead treats a changed line as one deletion plus one insertion. These models answer different questions: with substitution cost 1, changing one token costs 1; in an insert/delete-only script it costs 2.
LCS as a prefix dynamic program #
Define as the LCS length of prefixes and . An empty prefix has no nonempty common subsequence:
For nonempty prefixes,
The recurrence follows from the last token of an optimum:
- if , one optimum can end with that shared token, leaving an optimum for the two shorter prefixes;
- if they differ, an LCS cannot use both final tokens as the same last element, so it omits at least one of them.
Every dependency has smaller indices, so filling rows from top to bottom and columns from left to right is a valid topological order of the subproblems. There are states and constant work per state: time and, for the full table, space.
A small LCS table #
Compare ABC with BAC:
| B | A | C | ||
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | |
| A | 0 | 0 | 1 | 1 |
| B | 0 | 1 | 1 | 1 |
| C | 0 | 1 | 1 | 2 |
The bottom-right value is 2. Backtracking can produce either AC or BC; the tie at the interior states is real information, not an implementation error.
Edit distance as a shortest path through prefixes #
Define as the minimum cost to transform into . With unit-cost insertion, deletion, and substitution,
and
Here is 1 when proposition is true and 0 otherwise. The formula is exhaustive because the last operation of any transformation must be one of those three cases. Removing that last operation leaves an optimal—or improvable—solution to the corresponding smaller prefix problem.
This table is also a shortest-path computation on an acyclic grid:
- a vertical edge deletes a token from ;
- a horizontal edge inserts a token from ;
- a diagonal edge matches equal tokens at cost 0 or substitutes at cost 1.
Nonnegative operation-specific costs fit the same recurrence by replacing each +1. The model must still make semantic sense: a zero-cost edit can collapse distinctions, and negative costs make “perform extra edits” artificially profitable.
Worked trace: kitten to sitting #
| s | i | t | t | i | n | g | ||
|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | |
| k | 1 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| i | 2 | 2 | 1 | 2 | 3 | 4 | 5 | 6 |
| t | 3 | 3 | 2 | 1 | 2 | 3 | 4 | 5 |
| t | 4 | 4 | 3 | 2 | 1 | 2 | 3 | 4 |
| e | 5 | 5 | 4 | 3 | 2 | 2 | 3 | 4 |
| n | 6 | 6 | 5 | 4 | 3 | 3 | 2 | 3 |
The distance is 3. One optimal script is:
- substitute
kwiths:sitten; - substitute
ewithi:sittin; - insert
g:sitting.
The table certifies that no two-edit script exists; the displayed script certifies that three edits suffice. A distance without a script gives only the lower-bound side of that explanation.
The exact LCS–edit-script duality #
LCS and edit distance have a clean identity only for an insert/delete-only model. If , keep the matched tokens, delete the other tokens of , and insert the other tokens of . This yields
Conversely, every insert/delete script leaves some tokens untouched in their original order. Those survivors form a common subsequence, so a shorter script would imply a common subsequence longer than the LCS. The construction is therefore optimal in both directions.
The identity does not hold for unit-cost Levenshtein distance. A substitution can replace the delete-plus-insert pair for half the cost. It does hold if substitution is forbidden or assigned cost 2, although ties may then choose either representation.
Reconstructing an optimum #
A scalar answer needs only the final cell. An edit script or LCS needs the decisions that led there.
With the full table, start at and repeatedly move to a predecessor that justifies the current value:
- for LCS, a matching diagonal move emits the shared token; otherwise move toward a neighboring cell with the same maximum value;
- for edit distance, a zero-cost matching diagonal emits “keep,” while a predecessor satisfying the recurrence emits its substitution, deletion, or insertion.
Reverse the emitted operations at the end. Backtracking takes steps for one result.
Ties mean there are multiple optima. A reproducible tool must state a deterministic rule, such as preferring exact matches, then deletions, then insertions. That rule selects one proof path; it does not make the other optimal paths wrong. Enumerating all optima can require exponential output because the table may contain many branching ties.
Reducing memory without losing the objective #
Each DP row depends only on the previous row and the current row’s left neighbor. If only the distance or LCS length is required, keep two rows and place the shorter input on the column axis. Space falls to while time remains .
def levenshtein(a, b):
"""Unit-cost distance for any indexable token sequences."""
if len(b) > len(a):
a, b = b, a
previous = list(range(len(b) + 1))
for i, x in enumerate(a, start=1):
current = [i]
for j, y in enumerate(b, start=1):
current.append(min(
previous[j] + 1, # delete x
current[j - 1] + 1, # insert y
previous[j - 1] + (x != y),
))
previous = current
return previous[-1]This code intentionally cannot reconstruct a script: overwritten rows discard most proof edges.
Hirschberg’s algorithm recovers an LCS in linear working space. It splits one sequence near its midpoint, computes forward LCS lengths against every prefix of the other sequence, computes backward lengths against every suffix, and chooses a split maximizing the sum. It then recursively reconstructs the two halves. The subproblems across each recursion level cover only the original grid area, so the total remains time and auxiliary space, excluding the output.
This trade is useful when the table is the limiting resource. It may do more passes and have less favorable cache behavior than storing the full table, so “asymptotically less memory” does not imply “always faster.” For data larger than memory, layout and I/O become first-class concerns; see External-Memory Algorithms.
Myers’ edit graph for similar sequences #
For a diff with insertions and deletions, use coordinates for consumed prefixes of and . A deletion moves right, an insertion moves down, and a maximal run of equal tokens moves diagonally at zero edit cost. Myers’ algorithm explores this graph by increasing edit count .
Let diagonal . For each , keep the farthest reached on every feasible diagonal after exactly edits, then greedily extend the zero-cost matching diagonal—a snake. Reaching for the first time proves that is the minimum insert/delete script length, because all paths using fewer edits were exhausted in earlier layers.
If and the minimum script has length , the core method runs in time. This is attractive when two files are long but differ in only a few places; it can approach quadratic work when they are very different. Keeping one wavefront suffices for the distance in space. Straightforward path reconstruction retains historical wavefronts and costs more; Myers also gives a linear-space refinement.
The theoretical algorithm finds a shortest script. A production command named “Myers” may add heuristics or presentation policies. Current Git documentation explicitly distinguishes its default/basic greedy mode from minimal, which spends extra time to ensure the smallest diff, and from patience and histogram, which choose anchors differently. Treat a tool’s documented contract—not an algorithm family name—as authoritative.
Tokenization is part of correctness #
The recurrence compares tokens for equality; it cannot decide what a token ought to mean.
| Token unit | Useful for | Important boundary |
|---|---|---|
| byte | exact binary or encoded-file change | a visible character may occupy several bytes |
| Unicode code point | normalized textual data | a user-perceived character may contain several code points |
| grapheme cluster | user-visible text editing | segmentation and normalization policy must be explicit |
| word | prose-oriented review | punctuation and whitespace splitting change anchors |
| line | source patches and logs | a changed line is coarse; line endings and final newline matter |
| syntax node | refactoring-aware comparison | requires a parser and a language-specific edit model |
Unicode normalization is not automatically benign. Normalizing before comparison may be correct for search but incorrect for an exact patch, because it can hide a real encoding-level change. Likewise, ignoring whitespace can improve a review view while making the resulting script incapable of reproducing the target bytes.
Where the basic models stop fitting #
- Moves and renames: insert/delete distance represents a moved block as deletion plus insertion. Detecting moves is an additional inference problem.
- Transpositions: swapping adjacent tokens takes two substitutions under ordinary Levenshtein distance; Damerau-style models add a transposition operation and require a different recurrence.
- Biological alignment: substitution scores, affine gap-open/gap-extend penalties, local alignment, and evolutionary interpretation are not plain unit-cost edit distance.
- Structured documents: JSON, XML, or program syntax often needs tree-aware matching so that hierarchy and keys survive token movement.
- Very long sequences: time can be prohibitive even with linear memory. Banded DP is effective only when a credible distance bound exists; sparse or domain-indexed methods require additional assumptions.
- Similarity scores: raw distance grows with input length. Normalizing it creates a new metric contract and must specify the denominator and behavior on empty inputs.
If the only question is whether unit edit distance is at most , any successful path stays within . Computing only that diagonal band reduces work to —or reports immediately when . If every feasible path leaves the band, the correct conclusion is “distance exceeds ,” not a clipped distance value.
Choosing the method #
| Required result | Natural baseline | Time | Auxiliary space |
|---|---|---|---|
| LCS length | prefix DP with rolling rows | ||
| one LCS, memory available | full DP + backtrack | ||
| one LCS, memory constrained | Hirschberg | ||
| unit Levenshtein distance only | rolling Wagner–Fischer DP | ||
| explicit Levenshtein script | full DP + backtrack | ||
| short insert/delete diff between similar inputs | Myers edit-graph search | implementation-dependent | |
| human code review | documented diff policy + stable tokenization | policy-dependent | policy-dependent |
The baseline table is often the right first implementation: it is easy to verify and supports arbitrary local costs. Optimize only after the objective, input scale, and required output are fixed.
Failure patterns and diagnostics #
| Symptom | Likely cause | Check |
|---|---|---|
| distance is off by one near an empty input | wrong boundary row or column | verify and |
| LCS output is contiguous but too short | substring logic used by mistake | permit skips independently in both inputs |
| distance is right but script does not reproduce target | traceback operation or reversal error | replay every emitted edit on the original tokens |
| memory is still quadratic after “rolling rows” | parent table retained for reconstruction | separate scalar and script APIs |
| diffs jump between repeated lines across runs | unspecified tie-breaking or unstable iteration | make anchor and predecessor order deterministic |
| visually identical text has nonzero distance | encoding or normalization mismatch | inspect token sequence, not only rendered text |
| a moved block becomes one delete and one insert | operation model has no move | decide whether move detection is part of the contract |
Three invariants catch most implementation mistakes:
- every DP cell equals one of the recurrence’s justified alternatives;
- replaying a reconstructed script exactly produces the target sequence;
- for insert/delete-only comparison, .
Exercises #
- Fill the LCS table for
BANANAandATANA. Reconstruct two answers if a tie permits them. - Give insertion, deletion, and substitution costs . Explain why an optimum never needs a substitution when delete-plus-insert costs only 2.
- For two 1,000,000-line files with a ten-line insertion, compare the state counts suggested by full DP and by the edit-graph view.
- Construct two strings whose unit Levenshtein distance differs from their insert/delete distance.
Checks
- The LCS is
AANA, of length 4. It occurs at positions 2, 4, 5, 6 inBANANAand positions 1, 3, 4, 5 inATANA. The table contains ties, but they do not produce a second distinct length-4 token sequence. - Replacing one token can always be represented by deleting it and inserting the replacement for cost 2, so a cost-3 substitution is dominated.
- Full DP exposes about prefix pairs. Here and , so the Myers bound is on the order of wavefront work, ignoring constants and tokenization.
atobhas Levenshtein distance 1 but insert/delete distance 2.