Edit Distance (Levenshtein Distance)

Coding · Medium · Free problem

You are given two strings $s_1$ and $s_2$. You can transform $s_1$ into $s_2$ using three operations, each costing 1:

  • Insert a character at any position
  • Delete a character at any position
  • Substitute one character for another

Write a function that returns the minimum total number of operations needed to convert $s_1$ into $s_2$. This quantity is known as the Levenshtein (edit) distance.

Constraints:

  • $0 \leq |s_1|, |s_2| \leq 500$
  • Both strings consist of lowercase English letters

Examples:

1. s1 = "kitten", s2 = "sitting"3 - kitten → sitten (substitute 'k' with 's') - sitten → sittin (substitute 'e' with 'i') - sittin → sitting (insert 'g' at end)

2. s1 = "horse", s2 = "ros"3 - horse → rorse (substitute 'h' with 'r') - rorse → rose (delete 'r') - rose → ros (delete 'e')

  1. s1 = "", s2 = "abc"3 (3 insertions)

Hints

  1. Think about prefixes: define the subproblem as the edit distance between $s_1[0..i-1]$ and $s_2[0..j-1]$, and figure out how it relates to smaller subproblems.
  2. If the current characters match, you get them for free ($dp[i][j] = dp[i-1][j-1]$). If not, you pay 1 plus the cheapest of three options: delete, insert, or substitute.
  3. Set up the base cases first -- converting any string to an empty string costs exactly as many operations as its length -- then fill the $dp$ table row by row.

Worked Solution

How to Think About It: The naive move -- search over every sequence of edits -- is a trap: it branches exponentially. The heuristic that cracks it is optimal substructure via last-character alignment. Compare only the *last* characters of the two prefixes $s_1[0..i-1]$ and $s_2[0..j-1]$. Exactly one of four things is optimal for that final position: the two last characters already match (align them for free), or you paid for a delete, an insert, or a substitute. Each choice hands you back a *strictly smaller* prefix pair. That single recurrence is the whole problem; everything else is bookkeeping in an $(m{+}1)\times(n{+}1)$ table. This is the canonical DP / first-step-decomposition pattern -- decide the last operation, recurse on what remains.

Quick Estimate: Before writing code, bound the answer in your head. The edit distance is squeezed between $\big|\,|s_1|-|s_2|\,\big|$ (you must at least insert/delete the length difference) and $\max(|s_1|,|s_2|)$ (worst case: rewrite everything). For kitten (6) $\to$ sitting (7): lower bound $|6-7|=1$, upper bound $7$. The true answer $3$ sits comfortably inside. A sharper mental count: line the words up, sitt is shared in the middle, so you expect only a *handful* of edits -- 'k'$\to$'s', 'e'$\to$'i', and one trailing insert = $3$. For horse$\to$ros: length drop $5-3=2$ forces at least 2 deletes, and one letter differs, so $\approx 3$ -- exactly right. The bound-plus-eyeball is the sanity check an interviewer wants before you commit to the $O(mn)$ table.

Approach: Fill $dp[i][j]$ = edit distance of the first $i$ chars of $s_1$ and first $j$ chars of $s_2$, row by row, using the last-character recurrence.

Formal Solution:

Define $dp[i][j]$ = edit distance between $s_1[0..i-1]$ and $s_2[0..j-1]$.

*Base cases* (one string empty): - $dp[i][0] = i$ -- delete all $i$ characters of $s_1$. - $dp[0][j] = j$ -- insert all $j$ characters of $s_2$.

*Recurrence*, for $i,j \ge 1$: $$dp[i][j] = \begin{cases} dp[i-1][j-1] & \text{if } s_1[i-1] = s_2[j-1] \\[4pt] 1 + \min\!\big(dp[i-1][j],\; dp[i][j-1],\; dp[i-1][j-1]\big) & \text{otherwise} \end{cases}$$ where the three inner terms are delete from $s_1$, insert into $s_1$, and substitute respectively. The answer is $\boxed{dp[m][n]}$ with $m=|s_1|,\, n=|s_2|$.

```python def edit_distance(s1: str, s2: str) -> int: m, n = len(s1), len(s2) dp = [[0] * (n + 1) for _ in range(m + 1)] for i in range(m + 1): dp[i][0] = i for j in range(n + 1): dp[0][j] = j for i in range(1, m + 1): for j in range(1, n + 1): if s1[i - 1] == s2[j - 1]: dp[i][j] = dp[i - 1][j - 1] else: dp[i][j] = 1 + min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]) return dp[m][n] ```

Space optimization: each row depends only on the row above, so keep two rows and reduce space to $O(\min(m,n))$:

```python def edit_distance_optimized(s1: str, s2: str) -> int: if len(s1) < len(s2): s1, s2 = s2, s1 m, n = len(s1), len(s2) prev = list(range(n + 1)) for i in range(1, m + 1): curr = [i] + [0] * n for j in range(1, n + 1): if s1[i - 1] == s2[j - 1]: curr[j] = prev[j - 1] else: curr[j] = 1 + min(prev[j], curr[j - 1], prev[j - 1]) prev = curr return prev[n] ```

Complexity: Time $O(mn)$ (each of the $mn$ cells is $O(1)$); space $O(mn)$ for the full table, $O(\min(m,n))$ with two rows.

Answer: Edit distance is computed in $O(mn)$ time by DP; the key insight is that aligning the last characters of each prefix reduces the problem to three strictly smaller subproblems. On the examples: kitten$\to$sitting $=3$, horse$\to$ros $=3$, ""$\to$abc $=3$.

Intuition

Edit distance is the canonical example of optimal substructure in string problems. The reason DP works here is that any optimal alignment of the full strings must also optimally align every prefix pair along the way -- you cannot improve the total cost by making a locally suboptimal choice on a subproblem. Once you see that, the recurrence writes itself: match characters for free when you can, otherwise pay 1 and recurse on the best of three reduced problems.

In practice, edit distance (or variants of it) shows up in DNA sequence alignment, spell-checking, fuzzy string matching, and -- in quant contexts -- matching financial identifiers or instrument names across databases with messy, inconsistent formatting. The two-row space optimization is worth knowing: it cuts memory from $O(mn)$ to $O(n)$ with essentially no code complexity added, which matters when strings are large. The common mistake is forgetting the base cases -- leaving the first row and column of the table unfilled produces silently wrong answers for strings that differ in length.

Open the full interactive solver →