Edit Distance
Every time a spell checker suggests "beautiful" for "beutiful," some algorithm answers a key question. The same happens when a DNA sequencer aligns two genomes differing by a few mutations. The question is: how different are these two strings, really? Counting the number of characters that differ is not enough. "cat" and "act" have the same letters yet feel far apart. Meanwhile, "kitten" and "sitting" share a long common backbone despite several edits. We need a notion of distance that respects how strings transform into one another. This happens by inserting, deleting, or substituting single characters. We also need to find the cheapest way to do so. This is the edit distance problem. Vladimir Levenshtein formalized it in 1965. It is now central to bioinformatics, natural language processing, version control, and error-correcting codes. Git computes edit-distance-like diffs to show you what changed between commits. DNA and protein aligners use scored variants of this recurrence. They find evolutionary relationships between sequences this way. Spell checkers, autocomplete engines, and fuzzy search all rank candidate corrections. They do so by how few edits separate them from your typed text. The elegance of the solution is notable. A problem that looks combinatorially explosive collapses into something simple. There are exponentially many ways to interleave insertions, deletions, and substitutions. Yet it becomes a simple two-dimensional table filled in by dynamic programming. Once you have studied the Longest Common Subsequence problem, edit distance feels natural. It is almost a natural generalization of that idea. Instead of only allowing "skip a character in each string," we now allow substitutions too. Every operation carries a cost. This article builds the recurrence from first principles. It proves correctness via an exchange-style argument over optimal edit scripts. It walks through a full worked trace on "kitten" → "sitting." It discusses the tabulation-versus-memoization tradeoff. This tradeoff determines whether you need the full table. Alternatively, you may get away with space. It closes by pointing toward the string-matching algorithms that specialize these ideas. These include Knuth-Morris-Pratt String Matching, built for the exact-match regime.
- Edit distance is the minimum number of single-character insertions, deletions, and substitutions to turn one string into another.
- It is solved by a DP table = edit distance between the first and first characters of the two strings.
- The recurrence has three cases (insert, delete, substitute) plus a free "match" case, taking the minimum of the relevant costs.
- Time is and space is naively, but with rolling rows since each row depends only on the previous one.
- Correctness follows from an optimal-substructure argument: any optimal script's last operation reduces to one of three smaller subproblems.
- It generalizes Longest Common Subsequence (LCS is edit distance with only insert/delete, no substitution) and foreshadows more specialized string algorithms like Knuth-Morris-Pratt String Matching.
The edit distance between two prefixes is one plus the best of three smaller edit distances — insert, delete, or substitute — unless the last characters already match for free.
The idea
Given two strings and , the edit distance is the minimum number of elementary operations needed to transform into , where each operation is one of:
- Insertion: add a character to .
- Deletion: remove a character from .
- Substitution: replace one character of with another character.
This is often called the Levenshtein distance, after Vladimir Levenshtein, who introduced it in the context of error-correcting codes for binary strings. A sequence of operations transforming into is called an edit script, and an optimal edit script is one that achieves . The key structural fact is optimal substructure: if you look at the last operation in an optimal edit script that transforms into , it must be one of a small number of possibilities, and whichever one it is, the rest of the script must itself be an optimal transformation of a strictly smaller pair of prefixes. This is exactly the same flavor of argument used for Longest Common Subsequence, where the last characters of the two sequences either match (extend both) or one of them is dropped. Edit distance simply adds a fourth option — substitution — because unlike LCS, edit distance is allowed to actively change a character rather than only skip it. Formally, define for , . The base cases are immediate: turning an empty string into takes insertions, and turning into the empty string takes deletions. The general recurrence, justified in detail below, is
The three terms in the minimum correspond, respectively, to deleting , inserting , and substituting for .
How it works
We fill the table row by row (or column by column — the dependency structure allows either), since each cell depends only on the cell directly above, directly to the left, and diagonally above-left. This is the Memoization versus Tabulation choice made concrete: tabulation here is natural because the subproblem graph is a simple grid with no irregular recursion, so we can iterate in a fixed order instead of recursing with a cache. ``` function EDIT-DISTANCE(X[1..m], Y[1..n]): let D[0..m][0..n] be a new table for i from 0 to m: D[i][0] = i # delete all of X's first i chars for j from 0 to n: D[0][j] = j # insert all of Y's first j chars for i from 1 to m: for j from 1 to n: if X[i] == Y[j]: D[i][j] = D[i-1][j-1] # free match else: deleteCost = D[i-1][j] + 1 # delete X[i] insertCost = D[i][j-1] + 1 # insert Y[j] substituteCost = D[i-1][j-1] + 1 # replace X[i] with Y[j] D[i][j] = min(deleteCost, insertCost, substituteCost) return D[m][n] ``` To recover an actual optimal edit script (not just its length), record which case was chosen at each cell — a standard traceback — and walk from back to , emitting the corresponding operation at each step and moving to the predecessor cell. ``` function TRACEBACK(X, Y, D): i, j = m, n script = empty list while i > 0 or j > 0: if i > 0 and j > 0 and X[i] == Y[j] and D[i][j] == D[i-1][j-1]: script.prepend("match " + X[i]) i, j = i-1, j-1 else if i > 0 and j > 0 and D[i][j] == D[i-1][j-1] + 1: script.prepend("substitute " + X[i] + " -> " + Y[j]) i, j = i-1, j-1 else if i > 0 and D[i][j] == D[i-1][j] + 1: script.prepend("delete " + X[i]) i = i-1 else: script.prepend("insert " + Y[j]) j = j-1 return script ```
Complexity & correctness
Time complexity. The table has cells, and each cell is computed in time from at most three already-computed neighbors. Hence the total time is
This bound is tight for the DP approach: every cell must be visited at least once because, in the worst case (no shared structure between and ), each cell's value genuinely depends on distinguishing information not available from any proper subset of cells. There is no known algorithm that solves general edit distance in strongly subquadratic time; a celebrated result (Backurs–Indyk, 2015) shows that an algorithm for any would refute the Strong Exponential Time Hypothesis. So , while not proven optimal in the strongest sense, is believed to be essentially unimprovable in general.
Space complexity. Storing the full table costs space, which is also what the traceback procedure needs to recover an explicit edit script. If you only need the distance (not the script), note that row of depends only on row : rewrite the algorithm to keep just two rows (or one row updated in place with a temporary), reducing space to by always iterating over the shorter string's dimension as the row. This space-for-script tradeoff is a recurring theme in dynamic programming: the compact recurrence is cheap to evaluate, but reconstructing the witness (here, the actual sequence of edits) generally needs the full table, or a divide-and-conquer trick (Hirschberg's algorithm) to get the script in time but only space.
Correctness — proof sketch. We prove by strong induction on . Base cases. If , transforming the empty string into requires exactly insertions and no fewer (each insertion adds exactly one character, and we need more characters than we started with), so is correct. Symmetrically . Inductive step. Assume the recurrence is correct for all pairs of prefix-lengths summing to less than . Consider any optimal edit script transforming into , and examine its last operation:
- If and 's last operation is a match/no-op on these characters (or more precisely, if there's an optimal script that never touches the final matching characters): then removing that trivial step gives an optimal script transforming into , of cost , so . Conversely, any script for the smaller prefixes extends for free when the last characters already agree, so is achievable, and it cannot be less, because any transformation of into induces a transformation of the length- and length- prefixes (removing the free match cannot increase cost, by a standard argument bounding subsequence transformations by the whole). Hence equality.
- If the last operation is a deletion of : the script first optimally transforms into (cost by the inductive hypothesis, since ), then deletes , total cost .
- If the last operation is an insertion of : symmetric, the script optimally transforms into (cost ), then appends , total cost .
- If the last operation substitutes for (valid whenever , and never helpful when since that would waste an operation): the script optimally transforms into (cost ), then substitutes, total cost .
Since every edit script's last operation falls into one of these categories (an edit script is, by definition, a sequence of insert/delete/substitute operations, and the very last one applied must be one of the three), the optimal script achieves exactly
which matches the recurrence (the match case is the specialization where the substitution term is skipped in favor of the free diagonal move). By induction, equals the true edit distance for all , and in particular .
Relationship to LCS. If insertions and deletions cost 1 but substitutions are forbidden (or cost 2, i.e., "delete then insert"), the recurrence degenerates into exactly the Longest Common Subsequence recurrence in disguise: in that restricted cost model. Allowing genuine unit-cost substitution is what makes Levenshtein distance strictly more general and often smaller than this LCS-derived bound.
Worked example
Trace `EDIT-DISTANCE` on the classic pair () and (). The completed table (rows indexed by prefixes of `kitten`, columns by prefixes of `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 |
| k i | 2 | 2 | 1 | 2 | 3 | 4 | 5 | 6 |
| k i t | 3 | 3 | 2 | 1 | 2 | 3 | 4 | 5 |
| k i t t | 4 | 4 | 3 | 2 | 1 | 2 | 3 | 4 |
| k i t t e | 5 | 5 | 4 | 3 | 2 | 2 | 3 | 4 |
| k i t t e n | 6 | 6 | 5 | 4 | 3 | 3 | 2 | 3 |
Reading a few cells to confirm the recurrence:
- : `k`, `s`, mismatch. . ✓ matches table.
- : `i`, `i`, match. . ✓
- (bottom-right corner): the final answer, .
So , matching the well-known result. Running `TRACEBACK` recovers one optimal script:
| Step | Operation | Result |
|---|---|---|
| 1 | substitute `k` → `s` | sitten |
| 2 | substitute `e` → `i` | sittin |
| 3 | insert `g` at the end | sitting |
Three operations, matching , and indeed no shorter script exists — you cannot turn "kitten" into "sitting" in two edits, since the strings differ in at least three positions once you correctly account for the length change contributed by the extra `g`. A second, less symmetric example illustrates why substitution genuinely helps beyond plain LCS-style insert/delete. Take and . Their LCS is `a`,`c`,`e` or similar (length 3), which under the insert/delete-only cost model would suggest edits. But with substitution allowed, the DP finds a 3-edit script: substitute `b`→`z`, substitute `d`→`c` is invalid since positions shift — the actual DP correctly threads through matches at `a`, `c`(after shift), `e`, `d` alignment and lands on a distance of 3, strictly better than the insert/delete-only bound of 5. This gap is exactly why Levenshtein's model, not the pure LCS model, is the right tool when substitutions are cheap real-world edits (typos, point mutations).
Common pitfalls & traps
| Pitfall | Why it happens | Fix |
|---|---|---|
| Off-by-one indexing | Using 0-indexed strings but 1-indexed table without adjusting `X[i]` to `X[i-1]` | Be explicit about whether means "first characters" and offset string access accordingly |
| Forgetting the free-match case | Always charging even when | Check equality before computing the three costs; matched characters cost nothing |
| Wrong base case | Initializing but forgetting to fill the rest of row/column 0 | Explicitly loop-initialize both the first row (insertions) and first column (deletions) |
| Treating this as symmetric to LCS without substitution | Reusing an LCS recurrence (skip-one-or-skip-other) and missing the diagonal substitute term | Remember edit distance has four cases where LCS has three; substitution is what makes it strictly more powerful |
| Recomputing subproblems without memoizing (naive recursion) | Direct recursive translation of the recurrence, as in Memoization versus Tabulation discussions, without a cache | Either memoize with a hash/array cache, or tabulate bottom-up as shown here — never recurse raw |
| Assuming space also gives you the edit script for free | Rolling-row optimization discards earlier rows needed for traceback | Use full storage if you need the script, or apply Hirschberg's divide-and-conquer trick for space with the script recovered |
Pioneers & history
Origins · 1 minVladimir I. Levenshtein and how this was discovered
Levenshtein introduced what is now called Levenshtein distance in a 1965 paper on binary codes capable of correcting deletions, insertions, and reversals — a coding-theory problem, not originally a computer-science algorithms problem. The distance measure he defined to quantify how "close" two code-words could be under these corruptions became, over the following decades, the standard formalization of edit distance across bioinformatics, natural language processing, and software engineering, illustrating how a construct built for one purpose (error-correcting codes) can become foundational infrastructure for an entirely different field (approximate string matching).
Where this matters
Version control systems such as Git and diff tools compute edit-distance-like alignments to show minimal, human-readable changes between file revisions. Spell checkers and search-query "did you mean" features rank candidate corrections by Levenshtein distance from the misspelled input, often within a bounded radius (e.g., distance ) to keep suggestions fast and relevant. Bioinformatics tools for DNA, RNA, and protein sequence alignment — including the Needleman-Wunsch and Smith-Waterman algorithms — are direct descendants of this recurrence, with the cost matrix replaced by biologically motivated substitution scores (like BLOSUM or PAM matrices) instead of a flat unit cost. Fuzzy matching in databases, plagiarism detectors, OCR post-processing, and speech-recognition error analysis all use edit distance or its weighted variants as the core similarity metric. Because the exact quadratic-time DP does not scale to comparing millions of long sequences, real systems often combine it with filtering heuristics (k-mer indexing, banded DP restricted to a diagonal band when strings are known to be similar) or fall back to faster approximate algorithms — a design pattern that echoes how exact-match string search eventually motivates specialized linear-time algorithms like Knuth-Morris-Pratt String Matching when the problem is narrowed from "how different are these strings" to "does this exact pattern occur here."
:::quiz[What are the four cases in the edit-distance recurrence when filling cell D[i][j]?]
- Match, insert, delete, and swap-adjacent-characters
- Match (free), delete X[i], insert Y[j], substitute X[i] for Y[j] ✓
- Insert, delete, transpose, and reverse
- Match, insert, delete, and no-op only
Ask the tutor: How would the recurrence and complexity change if insertions, deletions, and substitutions each had different, arbitrary costs instead of all costing 1?
Is edit distance the same as Hamming distance?
No. Hamming distance only counts mismatched positions between two strings of equal length, with no insertions or deletions allowed — it's a special case that applies only when lengths already match and no shifting is needed. Edit distance (Levenshtein distance) handles strings of different lengths by allowing insertions and deletions in addition to substitutions, which is essential whenever a typo, mutation, or edit shifts the alignment of the remaining characters. If you tried to compute Hamming distance on "kitten" and "sitting" you couldn't even align them directly since they have different lengths, whereas edit distance handles this naturally by treating length differences as insertions or deletions.
Why does the recurrence charge substitution the same cost (1) as insertion or deletion?
In the classical unweighted Levenshtein model, every elementary operation costs exactly 1 by definition, which keeps the problem clean and gives a true metric (it satisfies the triangle inequality, symmetry, and identity of indiscernibles). Real applications often deviate from this: DNA alignment might charge a transition mutation less than a transversion, and OCR correction might charge visually similar substitutions (like "0" vs "O") less than dissimilar ones. The DP recurrence generalizes easily to arbitrary substitution costs and insertion/deletion costs , as long as they satisfy the triangle inequality if you want the result to remain a true metric; the same algorithm structure still applies unchanged.
Can edit distance be computed faster than O(mn) in general?
Not asymptotically, under standard complexity assumptions. Backurs and Indyk (2015) proved that an algorithm for any constant would violate the Strong Exponential Time Hypothesis, a widely believed conjecture about the hardness of Boolean satisfiability. Practical speedups exist for special cases: if you only care whether the distance is below some small threshold , a banded DP restricted to a diagonal band of width runs in time. Bit-parallel algorithms (like Myers' bit-vector algorithm) also give large constant-factor speedups without changing the asymptotic worst case.
How is edit distance related to Longest Common Subsequence?
Longest Common Subsequence is essentially edit distance restricted to only insertions and deletions, with substitutions disallowed or equivalently priced as a delete-then-insert pair. In that restricted cost model, , since every character not in the LCS must be either deleted from or inserted to form . Full Levenshtein distance, which permits a genuine unit-cost substitution, can be strictly smaller than this LCS-derived quantity whenever swapping single characters is cheaper than deleting one and inserting another, which is why edit distance is the more general and often more realistic model for typos and point mutations.
Why does the DP table need both a full row of insert-costs and full column of delete-costs as base cases?
The base cases encode the trivial fact that transforming an empty string into a string of length requires exactly insertions, and transforming a string of length into the empty string requires exactly deletions — there is no cheaper way to change length by than single-character insert/delete operations. Skipping this initialization, or initializing only without filling the rest of row 0 and column 0, breaks every downstream cell, since the recurrence for with reads from row or column , which eventually bottoms out at row 0 or column 0. It's a common but easy-to-catch bug: unit tests comparing an empty string against any nonempty string immediately expose a missing or wrong base case.
Does this algorithm give the actual edit script, or just the distance?
The forward DP pass computes only the numeric distance . To recover an actual sequence of insert/delete/substitute operations achieving that distance, you run a traceback: starting at cell , repeatedly check which of the recurrence's cases produced the current cell's value, emit the corresponding operation, and move to the predecessor cell, until reaching . This traceback needs the full table (or a divide-and-conquer variant like Hirschberg's algorithm, which recovers the script using only space by recursively finding a midpoint split), unlike the distance-only computation, which can use rolling rows.
Why is this called a "dynamic programming" algorithm rather than plain recursion?
The naive recursive definition of edit distance recomputes the same subproblems exponentially many times, because multiple different operation sequences can arrive at the same prefix pair — this is the overlapping-subproblems property. As discussed generally in Memoization versus Tabulation, dynamic programming exploits this by either caching recursive calls (memoization) or filling a table in dependency order (tabulation, as shown here), so each of the distinct subproblems is solved exactly once in additional work, giving the full bound instead of the exponential blowup of raw recursion.