119.Edit Distance
Problem
Given two strings word1 and word2, return the minimum number of operations needed to convert word1 into word2.
Allowed operations on word1, each costing 1:
- Insert a character
- Delete a character
- Replace a character
This is the Levenshtein distance between the two strings.
Examples
Input: word1 = "horse", word2 = "ros"
Output: 3
Explanation: horse → rorse (replace 'h' with 'r')
rorse → rose (delete 'r')
rose → ros (delete 'e')Input: word1 = "intention", word2 = "execution"
Output: 5
Explanation: intention → inention (delete 't') → enention (replace 'i'→'e')
→ exention (replace 'n'→'x') → exection (replace 'n'→'c')
→ execution (insert 'u')Input: word1 = "", word2 = "abc"
Output: 3
Explanation: Insert all three characters.Constraints
0 <= word1.length, word2.length <= 500word1andword2consist of lowercase English letters.
💡 Key Insight
Compare the last (or first) characters of the two prefixes. If they are equal, no operation is needed for them: dp[i][j] = dp[i-1][j-1]. Otherwise, one operation fixes the last position, and each operation maps to a smaller subproblem:
- replace
word1[i-1]withword2[j-1]→dp[i-1][j-1] - delete
word1[i-1]→dp[i-1][j] - insert
word2[j-1]at the end ofword1→dp[i][j-1]
So dp[i][j] = 1 + min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]). Base cases: turning a prefix of length i into "" costs i deletions, and "" into length j costs j inserts.
Recursion (Brute Force)
TimeO(3^(m+n))SpaceO(m + n)Intuition
solve(i, j) = minimum edits to turn word1[i..] into word2[j..]. If one suffix is empty, the cost is the length of the other. If the front characters match, move both. Otherwise try all three operations.
Algorithm
solve(i, j):- If
i == m, returnn - j(insert the rest). - If
j == n, returnm - i(delete the rest). - If
word1[i] == word2[j], returnsolve(i + 1, j + 1). - Return
1 + min(solve(i + 1, j + 1) /*replace*/, solve(i + 1, j) /*delete*/, solve(i, j + 1) /*insert*/).
- If
- Return
solve(0, 0).
Visualization
flowchart TD
A["solve(0,0)<br/>'ab' → 'b'<br/>a != b"] -->|"replace"| B["solve(1,1)<br/>'b' → '' = 1"]
A -->|"delete a"| C["solve(1,0)<br/>'b' → 'b' match"]
A -->|"insert b"| D["solve(0,1)<br/>'ab' → '' = 2"]
C --> E["solve(2,1) = 0"]
B --> F["1 + 1 = 2"]
E --> G["1 + 0 = 1 (best)"]
D --> H["1 + 2 = 3"]Code
class Solution {
public int minDistance(String word1, String word2) {
return solve(word1, word2, 0, 0);
}
private int solve(String a, String b, int i, int j) {
if (i == a.length()) {
return b.length() - j; // insert remaining chars of b
}
if (j == b.length()) {
return a.length() - i; // delete remaining chars of a
}
if (a.charAt(i) == b.charAt(j)) {
return solve(a, b, i + 1, j + 1); // free match
}
int replace = solve(a, b, i + 1, j + 1);
int delete = solve(a, b, i + 1, j);
int insert = solve(a, b, i, j + 1);
return 1 + Math.min(replace, Math.min(delete, insert));
}
}Complexity
- Time: O(3^(m+n)) — up to three branches per step.
- Space: O(m + n) — recursion depth.
Top-Down Memoization
TimeO(m · n).SpaceO(m · n)Intuition
There are only (m + 1) · (n + 1) distinct (i, j) states; memoize them.
Algorithm
memo[m][n]filled with-1(distances are never negative).- Same recursion with a cache check.
Dry Run
word1 = "horse", word2 = "ros"
solve(0,0): 'h' != 'r' → 1 + min(solve(1,1), solve(1,0), solve(0,1))
solve(1,1): 'o' == 'o' → solve(2,2)
solve(2,2): 'r' != 's' → 1 + min(solve(3,3)=2, solve(3,2), solve(2,3)=3)
...
solve(1,1) = 2, so solve(0,0) = 1 + 2 = 3Code
import java.util.Arrays;
class Solution {
private int[][] memo;
private String a, b;
public int minDistance(String word1, String word2) {
a = word1;
b = word2;
memo = new int[a.length()][b.length()];
for (int[] row : memo) {
Arrays.fill(row, -1);
}
return solve(0, 0);
}
private int solve(int i, int j) {
if (i == a.length()) {
return b.length() - j;
}
if (j == b.length()) {
return a.length() - i;
}
if (memo[i][j] != -1) {
return memo[i][j];
}
int res;
if (a.charAt(i) == b.charAt(j)) {
res = solve(i + 1, j + 1);
} else {
res = 1 + Math.min(solve(i + 1, j + 1), Math.min(solve(i + 1, j), solve(i, j + 1)));
}
memo[i][j] = res;
return res;
}
}Complexity
- Time: O(m · n).
- Space: O(m · n) — memo plus O(m + n) stack.
Bottom-Up 2-D Tabulation
TimeO(m · n).SpaceO(m · n).Intuition
dp[i][j] = edit distance between word1[0..i) and word2[0..j).
Recurrence:
dp[i][0] = i,dp[0][j] = jdp[i][j] = dp[i-1][j-1]ifword1[i-1] == word2[j-1]dp[i][j] = 1 + min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1])otherwise
Algorithm
- Allocate
dp[m+1][n+1], fill row 0 and column 0. - Fill the rest row by row.
- Return
dp[m][n].
Dry Run
word1 = "horse" (rows), word2 = "ros" (columns):
| "" | r | o | s | |
|---|---|---|---|---|
| "" | 0 | 1 | 2 | 3 |
| h | 1 | 1 | 2 | 3 |
| o | 2 | 2 | 1 | 2 |
| r | 3 | 2 | 2 | 2 |
| s | 4 | 3 | 3 | 2 |
| e | 5 | 4 | 4 | 3 |
dp[2][2] = dp[1][1] = 1 ('o' == 'o'), dp[4][3] = dp[3][2] = 2 ('s' == 's'), dp[5][3] = 1 + min(diag 3, up 2, left 4) = 3 — the best move is "up", i.e. delete 'e'.
Code
class Solution {
public int minDistance(String word1, String word2) {
int m = word1.length(), n = word2.length();
int[][] dp = new int[m + 1][n + 1];
for (int i = 0; i <= m; i++) {
dp[i][0] = i; // delete all i chars
}
for (int j = 0; j <= n; j++) {
dp[0][j] = j; // insert all j chars
}
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1]; // no cost
} else {
dp[i][j] = 1 + Math.min(dp[i - 1][j - 1], // replace
Math.min(dp[i - 1][j], dp[i][j - 1])); // delete / insert
}
}
}
return dp[m][n];
}
}Complexity
- Time: O(m · n).
- Space: O(m · n).
Space-Optimized 1-D DPOptimal
TimeO(m · n).SpaceO(n)Intuition
Each cell uses up (dp[i-1][j]), left (dp[i][j-1]) and up-left (dp[i-1][j-1]). With one array: before overwriting dp[j] it is "up", dp[j-1] is already "left", and the old dp[j-1] (saved in prevDiag) is "up-left". At the start of each row, dp[0] becomes i.
Algorithm
dp[j] = jforj = 0..n.- For
i = 1..m:prevDiag = dp[0];dp[0] = i; forj = 1..n:up = dp[j].- If chars equal:
dp[j] = prevDiag; elsedp[j] = 1 + min(prevDiag, up, dp[j-1]). prevDiag = up.
- Return
dp[n].
Dry Run
flowchart TD
A["init: dp = [0,1,2,3]<br/>cols '' r o s"] -->|"row h"| B["dp = [1,1,2,3]"]
B -->|"row o (o==o uses diag 1)"| C["dp = [2,2,1,2]"]
C -->|"row r (r==r uses diag 2)"| D["dp = [3,2,2,2]"]
D -->|"row s (s==s uses diag 2)"| E["dp = [4,3,3,2]"]
E -->|"row e"| F["dp = [5,4,4,3]"]
F --> G["answer dp[3] = 3"]Code
class Solution {
public int minDistance(String word1, String word2) {
int m = word1.length(), n = word2.length();
int[] dp = new int[n + 1];
for (int j = 0; j <= n; j++) {
dp[j] = j; // "" → word2[0..j)
}
for (int i = 1; i <= m; i++) {
int prevDiag = dp[0]; // dp[i-1][0]
dp[0] = i; // word1[0..i) → ""
for (int j = 1; j <= n; j++) {
int up = dp[j]; // dp[i-1][j]
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[j] = prevDiag;
} else {
dp[j] = 1 + Math.min(prevDiag, Math.min(up, dp[j - 1]));
}
prevDiag = up; // becomes up-left for column j+1
}
}
return dp[n];
}
}Complexity
- Time: O(m · n).
- Space: O(n) — one row (iterate over the shorter word as columns for O(min(m, n))).
Complexity Comparison
| Approach | Time | Space |
|---|---|---|
| Recursion (Brute Force) | O(3^(m+n)) | O(m + n) |
| Top-Down Memoization | O(m · n) | O(m · n) |
| Bottom-Up 2-D Tabulation | O(m · n) | O(m · n) |
| Space-Optimized 1-D DP | O(m · n) | O(n) |
Common Pitfalls
- Initializing row 0 / column 0 to
0— they must bejandi(pure inserts / deletes). - Adding
1on a match — equal characters cost nothing. - Mixing up which neighbour is insert vs delete; the result is still correct as long as all three neighbours are in the
min, but understanding the mapping helps in interviews. - In the 1-D version, forgetting to set
dp[0] = iat the start of each row or forgetting to saveprevDiagbefore overwriting. - Empty strings: if either word is empty the answer is the other's length — make sure your loops handle
m = 0orn = 0.