Edit Distance
MedPrompt
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.
Approaches
4 approaches with complexity analysis and trade-offs.
This approach directly translates the problem's definition into a recursive function. We explore all possible sequences of operations (insert, delete, replace) by considering the last characters of the two strings at each step. The function solve(i, j) calculates the edit distance for prefixes word1[0...i] and word2[0...j] by breaking it down into smaller subproblems.
Algorithm
- Define a recursive function
solve(i, j)that takes two indices representing the end of the prefixes ofword1andword2. - Base Case 1: If
iis less than 0 (meaningword1's prefix is empty), we need to insert all remaining characters ofword2. The cost isj + 1. - Base Case 2: If
jis less than 0 (meaningword2's prefix is empty), we need to delete all remaining characters ofword1. The cost isi + 1. - Recursive Step (Characters Match): If
word1.charAt(i)is the same asword2.charAt(j), no operation is needed for these characters. We simply recurse on the smaller prefixes:solve(i - 1, j - 1). - Recursive Step (Characters Differ): If the characters are different, we must perform an operation. We explore all three possibilities and take the one with the minimum cost:
- Insert:
1 + solve(i, j - 1) - Delete:
1 + solve(i - 1, j) - Replace:
1 + solve(i - 1, j - 1)
- Insert:
- The initial call to the function is
solve(word1.length() - 1, word2.length() - 1).
Walkthrough
The core idea is to make a decision at each step based on the last characters of the current prefixes of word1 and word2.
If the characters word1[i] and word2[j] match, they contribute nothing to the edit distance. The problem reduces to finding the edit distance for the strings without these last characters, which is a recursive call solve(i-1, j-1).
If the characters do not match, we have three choices:
- Insert: We can insert
word2[j]intoword1to match it. The problem then becomes convertingword1[0...i]toword2[0...j-1]. The cost is1 + solve(i, j-1). - Delete: We can delete
word1[i]. The problem becomes convertingword1[0...i-1]toword2[0...j]. The cost is1 + solve(i-1, j). - Replace: We can replace
word1[i]withword2[j]. The problem becomes convertingword1[0...i-1]toword2[0...j-1]. The cost is1 + solve(i-1, j-1).
We take the minimum of these three options. The base cases handle situations where one of the strings becomes empty.
class Solution { public int minDistance(String word1, String word2) { return solve(word1, word2, word1.length() - 1, word2.length() - 1); } private int solve(String word1, String word2, int i, int j) { // Base case: If word1 is empty, we need to insert all characters of word2. if (i < 0) { return j + 1; } // Base case: If word2 is empty, we need to delete all characters of word1. if (j < 0) { return i + 1; } // If the last characters are the same, no operation is needed. if (word1.charAt(i) == word2.charAt(j)) { return solve(word1, word2, i - 1, j - 1); } else { // If last characters are different, consider all three operations. int insertOp = solve(word1, word2, i, j - 1); int deleteOp = solve(word1, word2, i - 1, j); int replaceOp = solve(word1, word2, i - 1, j - 1); return 1 + Math.min(insertOp, Math.min(deleteOp, replaceOp)); } }}Complexity
Time
O(3^max(m, n))
Space
O(m + n)
Trade-offs
Pros
Simple to understand and implement as it directly follows the problem's recursive structure.
Cons
Extremely inefficient due to a large number of redundant calculations for the same subproblems.
Will result in a 'Time Limit Exceeded' (TLE) error on most platforms for anything but very small inputs.
This approach, also known as Top-Down Dynamic Programming, optimizes the brute-force recursion. It uses a 2D array (a memoization table) to store the results of subproblems that have already been solved. When the function is called with the same parameters again, it retrieves the result from the table instead of re-computing it, thus avoiding exponential complexity.
Algorithm
- Create a 2D array
memoof size(m+1) x (n+1)to store the results of subproblems, initialized with a value like -1. - Define a recursive function
solve(i, j)that takes the current lengths of the prefixes. - Base Cases: If
iis 0, returnj. Ifjis 0, returni. - Memoization Check: Before computing, check if
memo[i][j]is not -1. If it's not, return the stored value immediately. - Recursive Step (Characters Match): If
word1.charAt(i-1) == word2.charAt(j-1), the result issolve(i-1, j-1). - Recursive Step (Characters Differ): Otherwise, the result is
1 + min(solve(i, j-1), solve(i-1, j), solve(i-1, j-1)). - Store Result: Before returning the computed result, store it in
memo[i][j]. - The initial call is
solve(m, n).
Walkthrough
We observe that the brute-force recursive solution repeatedly solves the same subproblems (e.g., solve(i, j) is called from multiple different paths). Memoization addresses this by caching the results.
We use a 2D array, memo, where memo[i][j] will store the minimum edit distance between the first i characters of word1 and the first j characters of word2. The array is initialized with a sentinel value (like -1) to indicate that a state has not been computed.
In our recursive function, the first step is to check the memo table. If a result for the current state (i, j) exists, we return it instantly. Otherwise, we perform the same logic as the brute-force approach to compute the result. Crucially, before returning, we save this newly computed result in memo[i][j] for future use.
This ensures that each of the m * n subproblems is calculated only once.
class Solution { public int minDistance(String word1, String word2) { int m = word1.length(); int n = word2.length(); int[][] memo = new int[m + 1][n + 1]; for (int[] row : memo) { java.util.Arrays.fill(row, -1); } return solve(word1, word2, m, n, memo); } private int solve(String word1, String word2, int i, int j, int[][] memo) { if (i == 0) { return j; } if (j == 0) { return i; } if (memo[i][j] != -1) { return memo[i][j]; } int result; if (word1.charAt(i - 1) == word2.charAt(j - 1)) { result = solve(word1, word2, i - 1, j - 1, memo); } else { int insertOp = solve(word1, word2, i, j - 1, memo); int deleteOp = solve(word1, word2, i - 1, j, memo); int replaceOp = solve(word1, word2, i - 1, j - 1, memo); result = 1 + Math.min(insertOp, Math.min(deleteOp, replaceOp)); } memo[i][j] = result; return result; }}Complexity
Time
O(m * n)
Space
O(m * n)
Trade-offs
Pros
Drastically more efficient than brute-force, with a polynomial time complexity.
Often more intuitive to derive from the recursive solution.
Cons
Uses O(m * n) space for the memoization table, which can be large.
Still uses recursion, which has some overhead and a limited stack depth (though not an issue for the given constraints).
This is the classic iterative or Bottom-Up Dynamic Programming solution. It avoids recursion by systematically building up the solution from the smallest subproblems. A 2D table, dp, is used where dp[i][j] stores the minimum edit distance to convert the first i characters of word1 to the first j characters of word2.
Algorithm
- Let
m = word1.length()andn = word2.length(). - Create a 2D array
dpof size(m+1) x (n+1). - Initialize Base Cases:
- Fill the first row:
dp[0][j] = jforjfrom 0 ton. (Cost of insertions) - Fill the first column:
dp[i][0] = iforifrom 0 tom. (Cost of deletions)
- Fill the first row:
- Fill the Table:
- Iterate with
ifrom 1 tom. - Iterate with
jfrom 1 ton. - If
word1.charAt(i-1) == word2.charAt(j-1):dp[i][j] = dp[i-1][j-1](No operation cost)
- Else:
dp[i][j] = 1 + min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])(Cost of delete, insert, or replace)
- Iterate with
- The final answer is the value in the bottom-right cell,
dp[m][n].
Walkthrough
The tabulation approach constructs the solution iteratively. We create a dp table of size (m+1) x (n+1).
First, we establish the base cases. dp[0][j] represents converting an empty string to a prefix of word2 of length j, which requires j insertions. So, dp[0][j] = j. Similarly, dp[i][0] represents converting a prefix of word1 of length i to an empty string, requiring i deletions. So, dp[i][0] = i.
Then, we fill the rest of the table row by row, column by column. To compute dp[i][j], we look at the characters word1[i-1] and word2[j-1].
- If they match, the cost is the same as the subproblem for prefixes
i-1andj-1, sodp[i][j] = dp[i-1][j-1]. - If they differ, we need one operation. The total cost will be 1 plus the minimum of the costs of the three preceding states:
dp[i-1][j]: Cost if we deleteword1[i-1].dp[i][j-1]: Cost if we insertword2[j-1].dp[i-1][j-1]: Cost if we replaceword1[i-1]withword2[j-1].
The final answer is dp[m][n], which represents the edit distance for the full strings.
class Solution { public int minDistance(String word1, String word2) { int m = word1.length(); int n = word2.length(); int[][] dp = new int[m + 1][n + 1]; // Initialize base cases for (int i = 0; i <= m; i++) { dp[i][0] = i; // Cost of deleting i chars from word1 to get an empty string } for (int j = 0; j <= n; j++) { dp[0][j] = j; // Cost of inserting j chars to an empty string to get word2 } // Fill the DP table 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]; } else { int deleteOp = dp[i - 1][j]; int insertOp = dp[i][j - 1]; int replaceOp = dp[i - 1][j - 1]; dp[i][j] = 1 + Math.min(deleteOp, Math.min(insertOp, replaceOp)); } } } return dp[m][n]; }}Complexity
Time
O(m * n)
Space
O(m * n)
Trade-offs
Pros
Efficient and robust, with no recursion overhead or risk of stack overflow.
The logic is clear and directly models the state transitions.
Cons
Requires O(m * n) space, which can be memory-intensive for very long strings.
This approach optimizes the space complexity of the bottom-up DP solution from O(m*n) to O(min(m, n)). It's based on the observation that to compute the values for the current row i of the DP table, we only need the values from the previous row i-1. Therefore, we don't need to store the entire 2D table, and can make do with a single 1D array.
Algorithm
- To minimize space, ensure
word2is the shorter string (if not, swap the strings). Letmbe the length of the longer string andnbe the length of the shorter one. - Create a 1D array
dpof sizen+1. - Initialize
dpas the first row of the conceptual 2D table:dp[j] = jforjfrom 0 ton. - Iterate
ifrom 1 tom(for each character of the longer string):- Store the top-left diagonal value needed for the first column calculation:
prev_val = dp[0]. - Update the first element of the current row:
dp[0] = i. - Iterate
jfrom 1 ton:- Store the value from the previous row,
dp[j], in atempvariable before it's overwritten. This is thedp[i-1][j]value. - If
word1.charAt(i-1) == word2.charAt(j-1), thendp[j] = prev_val. - Else,
dp[j] = 1 + min(dp[j-1], temp, prev_val). - Update
prev_val = tempto be used in the nextjiteration as the new diagonal value.
- Store the value from the previous row,
- Store the top-left diagonal value needed for the first column calculation:
- Return
dp[n].
Walkthrough
Instead of a full 2D table, we can use a single 1D array, say dp, of size n+1 (where n is the length of the shorter string to save space). This dp array will be used to compute the current row's values based on the previous row's values which it already holds.
When we compute dp[j] (representing the cell (i, j) in the 2D version), we need:
dp[i][j-1](Insert): This isdp[j-1]in our 1D array, which has just been computed for the current rowi.dp[i-1][j](Delete): This is the value thatdp[j]held before we started computing the current rowi. We save this in atempvariable.dp[i-1][j-1](Replace/Match): This was the value ofdp[j-1]from the previous row. We use another variable,prev_val, to track this diagonal value as we iterate across the row.
By carefully managing prev_val and a temp variable inside the inner loop, we can simulate the 2D table's calculations using only one array.
class Solution { public int minDistance(String word1, String word2) { int m = word1.length(); int n = word2.length(); // Ensure word2 is the shorter string to optimize space if (m < n) { // Swap strings and lengths to call recursively return minDistance(word2, word1); } // Now m >= n. We use an array of size n+1. int[] dp = new int[n + 1]; // Initialize the dp array as if word1 is empty for (int j = 0; j <= n; j++) { dp[j] = j; } // Iterate through word1 for (int i = 1; i <= m; i++) { int prev_val = dp[0]; // This is dp[i-1][j-1] for j=1 dp[0] = i; // Base case for the current row i // Iterate through word2 for (int j = 1; j <= n; j++) { int temp = dp[j]; // This is dp[i-1][j] if (word1.charAt(i - 1) == word2.charAt(j - 1)) { dp[j] = prev_val; } else { // dp[j-1] is dp[i][j-1] (insert) // temp is dp[i-1][j] (delete) // prev_val is dp[i-1][j-1] (replace) dp[j] = 1 + Math.min(dp[j - 1], Math.min(temp, prev_val)); } prev_val = temp; } } return dp[n]; }}Complexity
Time
O(m * n)
Space
O(min(m, n))
Trade-offs
Pros
Most efficient solution in terms of space complexity.
Maintains the optimal O(m * n) time complexity.
Cons
The logic is more complex to implement correctly due to the need to juggle values from the previous and current rows within a single array.
Solutions
Solution
class Solution {public int minDistance(String word1, String word2) { int m = word1.length(), n = word2.length(); int[][] f = new int[m + 1][n + 1]; for (int j = 1; j <= n; ++j) { f[0][j] = j; } for (int i = 1; i <= m; ++i) { f[i][0] = i; for (int j = 1; j <= n; ++j) { if (word1.charAt(i - 1) == word2.charAt(j - 1)) { f[i][j] = f[i - 1][j - 1]; } else { f[i][j] = Math.min(f[i - 1][j], Math.min(f[i][j - 1], f[i - 1][j - 1])) + 1; } } } return f[m][n]; }}/** * @param {string} word1 * @param {string} word2 * @return {number} */ var minDistance = function (word1, word2) { const m = word1.length; const n = word2.length; const f = Array(m + 1) .fill(0) .map(() => Array(n + 1).fill(0)); for (let j = 1; j <= n; ++j) { f[0][j] = j; } for (let i = 1; i <= m; ++i) { f[i][0] = i; for (let j = 1; j <= n; ++j) { if (word1[i - 1] === word2[j - 1]) { f[i][j] = f[i - 1][j - 1]; } else { f[i][j] = Math.min(f[i - 1][j], f[i][j - 1], f[i - 1][j - 1]) + 1; } } } return f[m][n]; };class Solution {public: int minDistance(string word1, string word2) { int m = word1.size(), n = word2.size(); int f[m + 1][n + 1]; for (int j = 0; j <= n; ++j) { f[0][j] = j; } for (int i = 1; i <= m; ++i) { f[i][0] = i; for (int j = 1; j <= n; ++j) { if (word1[i - 1] == word2[j - 1]) { f[i][j] = f[i - 1][j - 1]; } else { f[i][j] = min({f[i - 1][j], f[i][j - 1], f[i - 1][j - 1]}) + 1; } } } return f[m][n]; }};class Solution: # dp[i - 1][j - 1]) meaning replace. # e.g. "abc" and "bf", checking "ab" and "b" distance, # then replace either way for "c" or "f" dp [ i ][ j ] = 1 + min ( dp [ i ][ j - 1 ], dp [ i - 1 ][ j ], dp [ i - 1 ][ j - 1 ]) return dp [ - 1 ][ - 1 ] ############ class Solution : def minDistance ( self , word1 : str , word2 : str ) -> int : m , n = len ( word1 ), len ( word2 ) f = [[ 0 ] * ( n + 1 ) for _ in range ( m + 1 )] for j in range ( 1 , n + 1 ): f [ 0 ][ j ] = j for i , a in enumerate ( word1 , 1 ): f [ i ][ 0 ] = i # merged for loops from above solution, but not good for readability for j , b in enumerate ( word2 , 1 ): if a == b : f [ i ][ j ] = f [ i - 1 ][ j - 1 ] else : f [ i ][ j ] = min ( f [ i - 1 ][ j ], f [ i ][ j - 1 ], f [ i - 1 ][ j - 1 ]) + 1 return f [ m ][ n ] def minDistance(self, word1: str, word2: str) -> int: m, n = len(word1), len(word2) 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 word1[i - 1] == word2[j - 1]: dp[i][j] = dp[i - 1][j - 1] else:Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.