Problem overview
The function minDistance receives two strings, word1 and word2, and returns the fewest operations that turn word1 into word2. Three operations exist and each costs one: insert a character anywhere, delete a character, or replace a character with another one. Either word can be empty, and both use only lowercase letters.
For "horse" and "ros" the answer is 3: replace h with r to get "rorse", delete the second r to get "rose", delete e to get "ros". For "intention" and "execution" it is 5, and for "" and "abc" it is 3, three inserts.
Approach: solve for every pair of prefixes
Trying every sequence of operations is far too slow, since each step has many choices. The statement asks for O(m * n) time, where m and n are the word lengths, which points to solving smaller versions of the problem and reusing them.
Let dp[i][j] be the fewest operations that turn the first i characters of word1 into the first j characters of word2. The table has (m + 1) * (n + 1) entries, and the answer is dp[m][n], the entry for both whole words.
The first row and column are the easy cases, where one prefix is empty. 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.
For the rest, look at the last characters of the two prefixes, word1[i - 1] and word2[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 must fix the end, and there are three ways to do it:
- replace:
dp[i - 1][j - 1], with both prefixes one character shorter; - delete the character from
word1:dp[i - 1][j]; - insert
word2's last character at the end:dp[i][j - 1], since the new character matches and onlyword2's prefix gets shorter.
dp[i][j]is 1 plus the smallest of those three. - replace:
Filling the table row by row and left to right means all three neighbours are ready when an entry is computed. Here is the table for "cat" and "cut". Rows are the prefixes of "cat" from empty to whole, columns the prefixes of "cut":
0 1 2 3
1 0 1 2
2 1 1 2
3 2 2 1
The bottom-right entry is 1: replace a with u.
Step by step
- Store the lengths of the words in
intvariablesmandn. They are used for the table size and the loops. - Create
dpas avector<vector<int>>withm + 1rows, each avector<int>ofn + 1zeros. The extra row and column stand for the empty prefix. - Fill column 0 with a loop over
ifrom 0 tom, settingdp[i][0]toi: that many deletes. - Fill row 0 with a loop over
jfrom 0 ton, settingdp[0][j]toj: that many inserts.dp[0][0]is set twice, both times to 0. - Loop over the rest of the table:
ifrom 1 tom, and inside itjfrom 1 ton. Row 0 and column 0 are done, so both start at 1. - If the last characters are equal, copy
dp[i - 1][j - 1]. The firsticharacters end at indexi - 1, because the string counts from 0 while the table counts characters. - Otherwise set
dp[i][j]to 1 plus the smallest of the replace, delete and insert neighbours. Theminfunction compares two values, so two nested calls pick the smallest of three. - Return
dp[m][n]after the loops.
The solution in C++
class Solution {
public:
int minDistance(string word1, string word2) {
int m = word1.size();
int n = word2.size();
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
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[i - 1] == word2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = 1 + min(dp[i - 1][j - 1], min(dp[i - 1][j], dp[i][j - 1]));
}
}
}
return dp[m][n];
}
};
The C++ details worth a closer look:
size()andint.word1.size()returns an unsigned type, and assigning it to anintconverts it. That is fine for words of up to 500 characters. The conversion matters elsewhere: for an empty string,word.size() - 1does not give -1 but 18446744073709551615, because unsigned arithmetic wraps. The code avoids that by storing the lengths asintand never subtracting fromsize().- Building the table.
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0))makesm + 1copies of one row ofn + 1zeros. The closing>>of the type is fine in C++11 and later. - Passing by value.
minDistancetakes the strings by value, so the function works on copies. It only reads them, so aconst string&would also work, but the signature stays as the lesson has it. word1[i - 1]reads achar, and comparing twocharvalues with==compares the characters themselves, not addresses.- Two
mincalls.std::mintakes two arguments. An initializer list version,min({a, b, c}), also exists, but nesting two calls is enough.
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 whole table is kept. Each row only reads itself and the row above, so two rows would be enough and bring the space down to O(n), but the full table is easier to follow.
Edge cases and common mistakes
- Empty words.
minDistance("", "abc")returns 3 andminDistance("abc", "")returns 3 as well. Two empty words give 0, because the table has a single entry,dp[0][0]. - Equal words.
"abc"and"abc"give 0: every entry on the diagonal copies the one before it. - Index off by one. The table entry
dp[i][j]is abouticharacters, so the last one isword1[i - 1]. Usingword1[i]compares the character after the one the entry ends on, and the answer is wrong. - Forgetting the 1. In the
elsebranch the operation itself costs one. Without it, mismatching characters would be free. - Starting the main loops at 0. Then
i - 1orj - 1is -1 and the code indexes before the start of the string and the row.
Testing
"horse","ros"gives3."intention","execution"gives5."","abc"gives3."abc",""gives3."a","b"gives1."ab","ba"gives2."abc","yabd"gives2.
