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 O(mn)O(mn) table. Alternatively, you may get away with O(min⁡(m,n))O(\min(m,n)) 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.

Key takeaways
  • 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 D[i][j]D[i][j] = edit distance between the first ii and first jj 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 O(mn)O(mn) and space is O(mn)O(mn) naively, but O(min⁡(m,n))O(\min(m,n)) 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 core insight

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 X=x1x2…xmX = x_1 x_2 \dots x_m and Y=y1y2…ynY = y_1 y_2 \dots y_n, the edit distance ed(X,Y)\mathrm{ed}(X, Y) is the minimum number of elementary operations needed to transform XX into YY, where each operation is one of:

  • Insertion: add a character to XX.
  • Deletion: remove a character from XX.
  • Substitution: replace one character of XX 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 XX into YY is called an edit script, and an optimal edit script is one that achieves ed(X,Y)\mathrm{ed}(X, Y). The key structural fact is optimal substructure: if you look at the last operation in an optimal edit script that transforms x1…xix_1 \dots x_i into y1…yjy_1 \dots y_j, 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 D[i][j]=ed(x1…xi, y1…yj)D[i][j] = \mathrm{ed}(x_1 \dots x_i,\ y_1 \dots y_j) for 0≤i≤m0 \le i \le m, 0≤j≤n0 \le j \le n. The base cases are immediate: turning an empty string into y1…yjy_1 \dots y_j takes jj insertions, and turning x1…xix_1 \dots x_i into the empty string takes ii deletions. The general recurrence, justified in detail below, is

D[i][j]={ji=0ij=0D[i−1][j−1]i,j>0, xi=yj1+min⁡(D[i−1][j], D[i][j−1], D[i−1][j−1])i,j>0, xi≠yjD[i][j] = \begin{cases} j & i = 0 \\ i & j = 0 \\ D[i-1][j-1] & i, j > 0,\ x_i = y_j \\ 1 + \min\big(D[i-1][j],\ D[i][j-1],\ D[i-1][j-1]\big) & i, j > 0,\ x_i \ne y_j \end{cases}

The three terms in the minimum correspond, respectively, to deleting xix_i, inserting yjy_j, and substituting xix_i for yjy_j.

How it works

We fill the (m+1)×(n+1)(m+1) \times (n+1) table DD 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 D[m][n]D[m][n] back to D[0][0]D[0][0], 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 ```

Figure
The edit-distance DP table as a grid, with arrows showing where each cell's value comes from
Cell D[i][j] depends on three neighbors D[i-1][j-1] D[i-1][j] D[i][j-1] D[i][j] diagonal → match (free) or substitute (+1) from above → delete X[i] (+1) from left → insert Y[j] (+1) Example: X="CA", Y="AC" ""AC "" C A 012 111 212

Complexity & correctness

Time complexity. The table has (m+1)(n+1)(m+1)(n+1) cells, and each cell is computed in O(1)O(1) time from at most three already-computed neighbors. Hence the total time is

O(mn).O(mn).

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 XX and YY), 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 O((mn)1−ϵ)O((mn)^{1-\epsilon}) algorithm for any ϵ>0\epsilon > 0 would refute the Strong Exponential Time Hypothesis. So O(mn)O(mn), while not proven optimal in the strongest sense, is believed to be essentially unimprovable in general.

Space complexity. Storing the full table costs O(mn)O(mn) 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 ii of DD depends only on row i−1i-1: rewrite the algorithm to keep just two rows (or one row updated in place with a temporary), reducing space to O(min⁡(m,n))O(\min(m, n)) 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 O(mn)O(mn) time but only O(min⁡(m,n))O(\min(m,n)) space.

Correctness — proof sketch. We prove D[i][j]=ed(x1…xi, y1…yj)D[i][j] = \mathrm{ed}(x_1\dots x_i,\ y_1 \dots y_j) by strong induction on i+ji + j. Base cases. If i=0i = 0, transforming the empty string into y1…yjy_1 \dots y_j requires exactly jj insertions and no fewer (each insertion adds exactly one character, and we need jj more characters than we started with), so D[0][j]=jD[0][j] = j is correct. Symmetrically D[i][0]=iD[i][0] = i. Inductive step. Assume the recurrence is correct for all pairs of prefix-lengths summing to less than i+ji+j. Consider any optimal edit script SS transforming x1…xix_1\dots x_i into y1…yjy_1 \dots y_j, and examine its last operation:

  • If xi=yjx_i = y_j and SS'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 x1…xi−1x_1 \dots x_{i-1} into y1…yj−1y_1 \dots y_{j-1}, of cost D[i−1][j−1]D[i-1][j-1], so D[i][j]≤D[i−1][j−1]D[i][j] \le D[i-1][j-1]. Conversely, any script for the smaller prefixes extends for free when the last characters already agree, so D[i][j]≤D[i−1][j−1]D[i][j] \le D[i-1][j-1] is achievable, and it cannot be less, because any transformation of x1…xix_1\dots x_i into y1…yjy_1 \dots y_j induces a transformation of the length-(i−1)(i-1) and length-(j−1)(j-1) 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 xix_i: the script first optimally transforms x1…xi−1x_1 \dots x_{i-1} into y1…yjy_1 \dots y_j (cost D[i−1][j]D[i-1][j] by the inductive hypothesis, since (i−1)+j<i+j(i-1)+j < i+j), then deletes xix_i, total cost D[i−1][j]+1D[i-1][j] + 1.
  • If the last operation is an insertion of yjy_j: symmetric, the script optimally transforms x1…xix_1\dots x_i into y1…yj−1y_1 \dots y_{j-1} (cost D[i][j−1]D[i][j-1]), then appends yjy_j, total cost D[i][j−1]+1D[i][j-1]+1.
  • If the last operation substitutes xix_i for yjy_j (valid whenever xi≠yjx_i \ne y_j, and never helpful when xi=yjx_i = y_j since that would waste an operation): the script optimally transforms x1…xi−1x_1 \dots x_{i-1} into y1…yj−1y_1 \dots y_{j-1} (cost D[i−1][j−1]D[i-1][j-1]), then substitutes, total cost D[i−1][j−1]+1D[i-1][j-1] + 1.

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

D[i][j]=min⁡(D[i−1][j]+1, D[i][j−1]+1, D[i−1][j−1]+[xi≠yj]),D[i][j] = \min\big(D[i-1][j] + 1,\ D[i][j-1]+1,\ D[i-1][j-1] + [x_i \ne y_j]\big),

which matches the recurrence (the match case is the xi=yjx_i = y_j specialization where the +1+1 substitution term is skipped in favor of the free diagonal move). By induction, D[i][j]D[i][j] equals the true edit distance for all i,ji, j, and in particular D[m][n]=ed(X,Y)D[m][n] = \mathrm{ed}(X, Y). ■\blacksquare

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: ed(X,Y)=m+n−2 LCS(X,Y)\mathrm{ed}(X,Y) = m + n - 2\,\mathrm{LCS}(X,Y) 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 X=kittenX = \texttt{kitten} (m=6m = 6) and Y=sittingY = \texttt{sitting} (n=7n = 7). The completed table D[i][j]D[i][j] (rows indexed by prefixes of `kitten`, columns by prefixes of `sitting`):

""sitting
""01234567
k11234567
k i22123456
k i t33212345
k i t t44321234
k i t t e55432234
k i t t e n66543323

Reading a few cells to confirm the recurrence:

  • D[1][1]D[1][1]: x1=x_1 = `k`, y1=y_1 = `s`, mismatch. min⁡(D[0][1]+1,D[1][0]+1,D[0][0]+1)=min⁡(2,2,1)=1\min(D[0][1]+1, D[1][0]+1, D[0][0]+1) = \min(2,2,1) = 1. ✓ matches table.
  • D[2][2]D[2][2]: x2=x_2 = `i`, y2=y_2 = `i`, match. D[2][2]=D[1][1]=1D[2][2] = D[1][1] = 1. ✓
  • D[6][7]D[6][7] (bottom-right corner): the final answer, 33.

So ed(kitten,sitting)=3\mathrm{ed}(\texttt{kitten}, \texttt{sitting}) = 3, matching the well-known result. Running `TRACEBACK` recovers one optimal script:

StepOperationResult
1substitute `k` → `s`sitten
2substitute `e` → `i`sittin
3insert `g` at the endsitting

Three operations, matching D[6][7]=3D[6][7] = 3, 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 X=abcdefX = \texttt{abcdef} and Y=azcedY = \texttt{azced}. Their LCS is `a`,`c`,`e` or similar (length 3), which under the insert/delete-only cost model would suggest 6+5−2⋅3=56 + 5 - 2 \cdot 3 = 5 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

PitfallWhy it happensFix
Off-by-one indexingUsing 0-indexed strings but 1-indexed table without adjusting `X[i]` to `X[i-1]`Be explicit about whether D[i][j]D[i][j] means "first ii characters" and offset string access accordingly
Forgetting the free-match caseAlways charging +1+1 even when xi=yjx_i = y_jCheck equality before computing the three costs; matched characters cost nothing
Wrong base caseInitializing D[0][0]=0D[0][0]=0 but forgetting to fill the rest of row/column 0Explicitly loop-initialize both the first row (insertions) and first column (deletions)
Treating this as symmetric to LCS without substitutionReusing an LCS recurrence (skip-one-or-skip-other) and missing the diagonal +1+1 substitute termRemember 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 cacheEither memoize with a hash/array cache, or tabulate bottom-up as shown here — never recurse raw
Assuming O(min⁡(m,n))O(\min(m,n)) space also gives you the edit script for freeRolling-row optimization discards earlier rows needed for tracebackUse full O(mn)O(mn) storage if you need the script, or apply Hirschberg's divide-and-conquer trick for O(min⁡(m,n))O(\min(m,n)) space with the script recovered

Pioneers & history

Origins · 1 minVladimir I. Levenshtein and how this was discovered
VI
Vladimir I. Levenshtein
1935–2017 · Soviet mathematician working in information theory and coding theory at the Keldysh Institute of Applied Mathematics

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).

History
1965
— Levenshtein defines the distance metric in the context of error-correcting codes for insertions, deletions, and reversals.
1974
— Wagner and Fischer publish "The String-to-String Correction Problem," giving the now-standard dynamic programming algorithm and formalizing its O(mn)O(mn) complexity.
1975
— Sellers extends the framework to weighted edit costs, generalizing it toward sequence alignment scoring used in molecular biology.
1981
— Smith and Waterman adapt edit-distance-style dynamic programming for local sequence alignment in molecular biology, seeding modern bioinformatics tools.
2015
— Backurs and Indyk prove a fine-grained conditional lower bound (based on SETH) showing truly subquadratic edit distance algorithms are unlikely to exist in general.

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 ≤2\le 2) 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 c(xi,yj)c(x_i, y_j) and insertion/deletion costs cins,cdelc_{\text{ins}}, c_{\text{del}}, as long as they satisfy the triangle inequality if you want the result to remain a true metric; the same O(mn)O(mn) 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 O((mn)1−ϵ)O((mn)^{1-\epsilon}) algorithm for any constant ϵ>0\epsilon > 0 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 kk, a banded DP restricted to a diagonal band of width O(k)O(k) runs in O(k⋅max⁡(m,n))O(k \cdot \max(m,n)) 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, ed(X,Y)=∣X∣+∣Y∣−2⋅LCS(X,Y)\mathrm{ed}(X,Y) = |X| + |Y| - 2 \cdot \mathrm{LCS}(X,Y), since every character not in the LCS must be either deleted from XX or inserted to form YY. 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 jj requires exactly jj insertions, and transforming a string of length ii into the empty string requires exactly ii deletions — there is no cheaper way to change length by kk than kk single-character insert/delete operations. Skipping this initialization, or initializing only D[0][0]=0D[0][0] = 0 without filling the rest of row 0 and column 0, breaks every downstream cell, since the recurrence for D[i][j]D[i][j] with i,j>0i,j>0 reads from row i−1i-1 or column j−1j-1, 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 D[m][n]D[m][n]. To recover an actual sequence of insert/delete/substitute operations achieving that distance, you run a traceback: starting at cell (m,n)(m,n), 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 (0,0)(0,0). This traceback needs the full O(mn)O(mn) table (or a divide-and-conquer variant like Hirschberg's algorithm, which recovers the script using only O(min⁡(m,n))O(\min(m,n)) 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 (i,j)(i,j) 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 (m+1)(n+1)(m+1)(n+1) distinct subproblems is solved exactly once in O(1)O(1) additional work, giving the full O(mn)O(mn) bound instead of the exponential blowup of raw recursion.

Before this

Where this leads