Edit Distance
You are given two strings word1 and word2. In a single operation you may insert one character into word1, delete one character from word1, or replace one character of word1 with any other character. Return the minimum number of operations needed to turn word1 into word2. Either string may be empty.
- 0 ≤ word1.length, word2.length ≤ 500
- Both strings consist of lowercase English letters
- The three permitted operations are insert, delete and replace, each costing 1
word1 = "horse", word2 = "ros"3word1 = "intention", word2 = "execution"5word1 = "", word2 = "abc"3word1 = "abc", word2 = "abc"0word1 = "a", word2 = "ab"1You may edit the first string one character at a time, and each edit costs exactly one. Insert a character anywhere, delete a character from anywhere, or replace one character with another. Question: what is the smallest number of edits that turns word1 into word2?
This number is called the Levenshtein distance, and it is what powers spell-checkers, fuzzy search and DNA sequence comparison. "horse" to "ros" takes 3: replace the h with r giving "rorse", delete the r giving "rose", delete the e giving "ros".
One clarification that saves a lot of confusion: although the operations are described as acting on word1, an insertion into word1 and a deletion from word2 are the same move viewed from opposite sides. The distance is symmetric — turning word1 into word2 costs exactly as much as the reverse.
The obvious plan is to try every sequence of edits and keep the shortest. At every position you have three choices, and strings are up to 500 characters, so the number of edit sequences is astronomically large. Worse, edits change the string underneath you, so it is not even clear what "position" means after a few of them.
The reframe that dissolves this: stop thinking about editing a string and start thinking about aligning two strings. Walk both strings from the left simultaneously and decide, at each step, how the current characters correspond. Under that view, editing never mutates anything — it just decides how the pointers advance.
State Definition: f(i, j) is the fewest operations to turn the first i characters of word1 into the first j characters of word2.
Look at the last character on each side, word1[i-1] and word2[j-1].
If they are the same, they already agree. Pay nothing and drop both: f(i-1, j-1). (Editing a pair that already matches never helps.)
If they differ, you must do one edit, and there are exactly three ways to make progress. Each costs 1, and each drops different characters:
word1's last character with word2's. Now they agree, so drop both: 1 + f(i-1, j-1).word1's last character. It is gone, so drop it from word1 while word2 still needs its character: 1 + f(i-1, j).word2's last character onto word1. That satisfies word2's character, so drop it from word2 while word1 is unchanged: 1 + f(i, j-1).Take the cheapest:
f(i, j) = 1 + min( f(i-1, j-1), f(i-1, j), f(i, j-1) ) when the characters differ.
word1, so i drops. Insert satisfies a character of word2, so j drops. Replace uses one from each, so both drop. If a move leaves both counts unchanged, the recursion never ends.Base cases:
f(0, j) = j — word1 is empty, so insert the j remaining characters of word2.f(i, 0) = i — word2 is empty, so delete the i remaining characters of word1.We start with the full lengths: f(n, m).
def min_distance(word1, word2):
n, m = len(word1), len(word2)
memo = {}
def f(i, j):
if i == 0: return j # insert the rest of word2
if j == 0: return i # delete the rest of word1
if (i, j) in memo: return memo[(i, j)]
if word1[i - 1] == word2[j - 1]:
result = f(i - 1, j - 1) # free
else:
result = 1 + min(f(i - 1, j - 1), # replace
f(i - 1, j), # delete from word1
f(i, j - 1)) # insert into word1
memo[(i, j)] = result
return result
return f(n, m)n × m states, constant work each: O(N × M) time and space. For 500-character strings that is 250000 cells.
Let dp[i][j] hold what f(i, j) returned, and swap calls for reads. The base cases are not zeros this time — they go into the first row and first column: dp[0][j] = j and dp[i][0] = i. Every read points at a smaller i or j, so both loops run upward from 1. The answer is dp[n][m]:
def min_distance(word1, word2):
n, m = len(word1), len(word2)
dp = [[0] * (m + 1) for _ in range(n + 1)]
for j in range(m + 1):
dp[0][j] = j # word1 empty: insert the rest
for i in range(n + 1):
dp[i][0] = i # word2 empty: delete the rest
for i in range(1, n + 1):
for j in range(1, m + 1):
if word1[i - 1] == word2[j - 1]:
dp[i][j] = dp[i - 1][j - 1]
else:
dp[i][j] = 1 + min(dp[i - 1][j - 1],
dp[i - 1][j],
dp[i][j - 1])
return dp[n][m]A cell only ever reads its own row and the row above, so two rows suffice and the space drops to O(M):
def min_distance(word1, word2):
n, m = len(word1), len(word2)
prev = list(range(m + 1)) # row 0: dp[0][j] = j
for i in range(1, n + 1):
cur = [0] * (m + 1)
cur[0] = i # dp[i][0] = i
for j in range(1, m + 1):
if word1[i - 1] == word2[j - 1]:
cur[j] = prev[j - 1]
else:
cur[j] = 1 + min(prev[j - 1], prev[j], cur[j - 1])
prev = cur
return prev[m]h vs r — differ. The winning branch is replace, costing 1 and moving both pointers. ("horse" → "rorse")o vs o — match, free, both advance.r vs s — differ. The winning branch is delete, costing 1 and moving only word1's pointer. ("rorse" → "rose")s vs s — match, free, both advance.e remains in word1 while word2 is exhausted, so the base case charges 1 deletion. ("rose" → "ros")Total 3.
Now a pure-insert case, "" to "abc": the first base case fires immediately and returns 3. And a case where replace is not the answer, "a" to "ab": replacing would cost 1 and still leave a mismatch downstream; inserting b costs 1 and finishes. The table returns 1.
The reflex to build: whenever a problem compares or transforms two sequences, set up the (i, j) grid, then spend your thinking on just two questions — what happens on a match, and what is the full list of moves on a mismatch.
Weighted variants (a deletion costs 2, an insertion costs 3) change nothing structurally: you stop adding 1 and start adding the specific cost. Restricted variants (say only deletions are allowed) simply remove entries from the mismatch menu. The grid and the walk stay the same; only the moves and their costs change.