Problem overview
The method minDistance(String word1, String word2) returns the fewest operations that turn word1 into word2. There are three operations, each costing one: insert a character anywhere, delete a character, or replace a character with another one.
For "horse" and "ros" the answer is 3: replace h with r to get "rorse", delete the second r to get "rose", then delete e. For "intention" and "execution" it is 5. For "" and "abc" it is 3: the empty word becomes "abc" by inserting three characters, and each operation adds at most one.
Approach: solve smaller versions first
Trying every sequence of operations is far too slow. Instead, solve the problem for every pair of prefixes. Let dp[i][j] be the fewest operations that turn the first i characters of word1 into the first j characters of word2. With m = word1.length() and n = word2.length(), the table has (m + 1) × (n + 1) entries and the answer is dp[m][n].
Row 0 and column 0 stand for an empty prefix. Turning i characters into nothing takes i deletes, so dp[i][0] = i. Turning nothing into j characters takes j inserts, so dp[0][j] = j.
The rest of the table is filled row by row, so the entries above, to the left and diagonally up-left of dp[i][j] are known by the time you reach it. Look at the last characters of the two prefixes, word1.charAt(i - 1) and word2.charAt(j - 1):
- They are equal. They need no operation, so
dp[i][j] = dp[i - 1][j - 1]. Spending an operation on either of them is never better. - They differ. One operation fixes the end, and
dp[i][j]is 1 plus the smallest of three neighbours.dp[i - 1][j - 1]is a replace: both prefixes become one character shorter.dp[i - 1][j]is a delete ofword1's last character.dp[i][j - 1]is an insert ofword2's last character at the end, which then matches, leaving a shorterword2prefix.
The table for "horse" and "ros"
Printing dp row by row for "horse" (rows) and "ros" (columns) gives:
[0, 1, 2, 3]
[1, 1, 2, 3]
[2, 2, 1, 2]
[3, 2, 2, 2]
[4, 3, 3, 2]
[5, 4, 4, 3]
The first row and column are the empty-prefix values. The bottom-right entry, dp[5][3], is 3, the answer. The entry at row 2, column 1 is 2: turning "ho" into "r" takes a replace and a delete. The entry at row 2, column 2 drops to 1, because the o in both words matches and dp[2][2] copies dp[1][1].
Step by step
- Store the lengths of the two words in
mandn. They come up often. - Create the table
dpasnew int[m + 1][n + 1]: one extra row and column for the empty prefixes. It starts filled with zeros. - Fill column 0. Loop
ifrom 0 tomand setdp[i][0] = i, the cost of deletingicharacters. - Fill row 0. Loop
jfrom 0 tonand setdp[0][j] = j, the cost of insertingjcharacters.dp[0][0]is set twice, both times to 0. - Loop over the rest of the table with
ifrom 1 tom, and inside itjfrom 1 ton. Row 0 and column 0 are already done. - Compare the last characters
word1.charAt(i - 1)andword2.charAt(j - 1). The firsticharacters end at indexi - 1: the word counts from 0, the table counts characters. If they are equal, copydp[i - 1][j - 1]. - Otherwise take 1 plus the smallest of the replace, delete and insert neighbours, using one
Math.mininside another, since each call compares only two values. - Return
dp[m][n].
The solution in Java
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];
for (int i = 0; i <= m; i++) {
dp[i][0] = i;
}
for (int j = 0; j <= n; j++) {
dp[0][j] = j;
}
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 {
dp[i][j] = 1 + Math.min(dp[i - 1][j - 1], Math.min(dp[i - 1][j], dp[i][j - 1]));
}
}
}
return dp[m][n];
}
}
The Java details worth a closer look:
- Two-dimensional arrays.
new int[m + 1][n + 1]creates an array ofm + 1rows, each anint[]ofn + 1zeros. You read an entry withdp[i][j]. charAtreturns achar. Comparing twocharvalues with==compares the characters themselves, which is correct. Comparing wholeStringobjects with==would compare references, so useequalsfor that.length()againstlength. AStringhas the methodlength(). An array has the fieldlength, with no parentheses.- Nested
Math.min.Math.mintakes exactly two arguments, so three candidates need two calls.
Complexity
Time: O(m · n). The table has (m + 1) × (n + 1) entries, and each takes a constant amount of work.
Space: O(m · n). The lesson keeps the whole table, which is easier to follow. Each row reads only itself and the row above it, so two rows of n + 1 numbers would be enough and would bring the space down to O(n).
Edge cases and common mistakes
- Empty words.
minDistance("", "abc")andminDistance("abc", "")both return 3, andminDistance("", "")returns 0. The loops over rows or columns still run for the empty prefix, and the double loop doesn't run at all. - Identical words.
minDistance("abc", "abc")is 0: every pair of last characters matches, and the diagonal copies 0. - Swapped letters are not one operation.
minDistance("ab", "ba")is 2, two replaces, because there is no "swap" operation. - Off-by-one on indexes.
dp[i][j]is about the firsticharacters, so the matching characters arecharAt(i - 1)andcharAt(j - 1). UsingcharAt(i)throws aStringIndexOutOfBoundsExceptionon the last row, whereiequals the word's length. - Table size. Making the table
mbynleaves no room for the empty prefixes. - Forgetting the base row and column. The inner loops would read zeros where the costs
iandjbelong, and every answer would come out too small.
Testing
"horse"to"ros"gives3."intention"to"execution"gives5."kitten"to"sitting"gives3."ab"to"ba"gives2.""to"abc"gives3."abc"to"abc"gives0.
