119. Edit Distance
View on NeetCode
View on LeetCode
Problem
Given two strings word1 and word2, return the minimum number of operations required to convert word1 to word2.
You have the following three operations permitted on a word:
- Insert a character
- Delete a character
- Replace a character
Example 1:
Input: word1 = "horse", word2 = "ros"
Output: 3
Explanation:
horse -> rorse (replace 'h' with 'r')
rorse -> rose (remove 'r')
rose -> ros (remove 'e')
Example 2:
Input: word1 = "intention", word2 = "execution"
Output: 5
Explanation:
intention -> inention (remove 't')
inention -> enention (replace 'i' with 'e')
enention -> exention (replace 'n' with 'x')
exention -> exection (replace 'n' with 'c')
exection -> execution (insert 'u')
Constraints:
0 <= word1.length, word2.length <= 500word1andword2consist of lowercase English letters.
Solution
Approach: 2D Dynamic Programming
The key insight is that at each position, we have three choices (insert, delete, replace), and we take the minimum cost among them.
Implementation
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
m, n = len(word1), len(word2)
# dp[i][j] = min operations to convert word1[0:i] to word2[0:j]
dp = [[0] * (n + 1) for _ in range(m + 1)]
# Base cases
for i in range(m + 1):
dp[i][0] = i # Delete all characters from word1
for j in range(n + 1):
dp[0][j] = j # Insert all characters from word2
for i in range(1, m + 1):
for j in range(1, n + 1):
if word1[i-1] == word2[j-1]:
# Characters match, no operation needed
dp[i][j] = dp[i-1][j-1]
else:
# Take minimum of:
# 1. Replace: dp[i-1][j-1] + 1
# 2. Delete from word1: dp[i-1][j] + 1
# 3. Insert to word1: dp[i][j-1] + 1
dp[i][j] = 1 + min(
dp[i-1][j-1], # Replace
dp[i-1][j], # Delete
dp[i][j-1] # Insert
)
return dp[m][n]
Approach 2: Space-Optimized 1D DP
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
m, n = len(word1), len(word2)
prev = list(range(n + 1))
for i in range(1, m + 1):
curr = [i] # First element is number of deletes
for j in range(1, n + 1):
if word1[i-1] == word2[j-1]:
curr.append(prev[j-1])
else:
curr.append(1 + min(
prev[j-1], # Replace
prev[j], # Delete
curr[j-1] # Insert
))
prev = curr
return prev[n]
Approach 3: Recursion with Memoization
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
memo = {}
def dp(i, j):
# Base cases
if i == 0:
return j # Insert all remaining chars from word2
if j == 0:
return i # Delete all remaining chars from word1
if (i, j) in memo:
return memo[(i, j)]
if word1[i-1] == word2[j-1]:
result = dp(i-1, j-1)
else:
result = 1 + min(
dp(i-1, j-1), # Replace
dp(i-1, j), # Delete
dp(i, j-1) # Insert
)
memo[(i, j)] = result
return result
return dp(len(word1), len(word2))
Complexity Analysis
2D DP:
- Time Complexity: O(m * n), where m and n are lengths of the two words.
- Space Complexity: O(m * n) for the dp table.
1D DP:
- Time Complexity: O(m * n).
- Space Complexity: O(n), only storing two rows.
Key Insights
-
Levenshtein Distance: This is the classic edit distance (Levenshtein distance) problem.
- Three Operations:
- Replace: Convert word1[i-1] to word2[j-1], cost 1 + dp[i-1][j-1]
- Delete: Remove word1[i-1], cost 1 + dp[i-1][j]
- Insert: Add word2[j-1], cost 1 + dp[i][j-1]
-
Character Match: When characters match, no operation needed: dp[i][j] = dp[i-1][j-1]
- Base Cases:
- Converting empty string to word2 requires j insertions
- Converting word1 to empty string requires i deletions
-
Optimal Substructure: The minimum operations for full strings depends on minimum operations for prefixes.
- Symmetry: The problem is symmetric - insert in word1 is equivalent to delete in word2 and vice versa.