Problem overview
minDistance(word1, word2) returns the fewest operations that turn word1 into word2. An operation is one of three, each costing 1: insert a character anywhere, delete a character, or replace a character with another one. Either word can be empty, and both use lowercase letters.
For "horse" and "ros" the answer is 3: replace h with r (rorse), delete the second r (rose), delete e (ros). For "intention" and "execution" it is 5. For "" and "abc" it is 3, three inserts.
Approach
Trying every sequence of operations explodes quickly. Instead, solve smaller versions of the same question and build up. 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, where m and n are the word lengths, and the answer is dp[m][n].
The borders
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 inside
Fill the table row by row, left to right. Then the entries above, to the left and diagonally up-left of dp[i][j] are already known. Compare the last characters of the two prefixes, word1[i - 1] and word2[j - 1]. The index is i - 1 because the first i characters end at index i - 1: the word counts from 0, the table counts characters.
- They are equal: no operation is needed for them, so
dp[i][j] = dp[i - 1][j - 1]. Spending an operation on either of them is never better. - They differ: one operation has to fix the end, and the cost is 1 plus the best of three neighbours:
dp[i - 1][j - 1]: replaceword1[i - 1]withword2[j - 1], leaving both prefixes one character shorter;dp[i - 1][j]: deleteword1[i - 1], so only the prefix ofword1shrinks;dp[i][j - 1]: insertword2[j - 1]at the end, which matches the last character ofword2's prefix, so only that prefix shrinks.
Step by step
- Lengths. Store
word1.lengthinmandword2.lengthinn, since they come up everywhere. - The table.
Array(m + 1) { IntArray(n + 1) }makesm + 1rows. The lambda runs once per row, so each row is its ownIntArrayofn + 1zeros. - Column 0. A loop with
iover0..msetsdp[i][0] = i. - Row 0. A loop with
jover0..nsetsdp[0][j] = j. The entrydp[0][0]is set twice, both times to 0. - The main loops.
igoes over1..mandjover1..n, because row 0 and column 0 are done. If a word is empty, the range such as1..0is empty and the loop does nothing. - Equal last characters. Copy
dp[i - 1][j - 1]. - Different last characters. This is the key line: 1 plus
minOfof the replace, delete and insert neighbours. - Return
dp[m][n].
The solution in Kotlin
class Solution {
fun minDistance(word1: String, word2: String): Int {
val m = word1.length
val n = word2.length
val dp = Array(m + 1) { IntArray(n + 1) }
for (i in 0..m) {
dp[i][0] = i
}
for (j in 0..n) {
dp[0][j] = j
}
for (i in 1..m) {
for (j in 1..n) {
if (word1[i - 1] == word2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1]
} else {
dp[i][j] = 1 + minOf(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1])
}
}
}
return dp[m][n]
}
}
Kotlin details worth knowing:
- The table must be built with a lambda.
Array(m + 1) { IntArray(n + 1) }calls the lambda for every row. WritingArray(m + 1) { shared }with oneIntArraynamedsharedwould put the same array in every row, and a change to one row would show in all of them. With such a table, setting[0][1]to 9 makes[1][1]read 9 too. word1[i - 1]is aChar. Indexing aStringgives aChar, and==compares the characters by value.minOftakes three arguments.minOf(a, b, c)exists forInt, so no nested calls are needed.- Ranges are inclusive.
0..mincludesm. A loop0 until mwould leave the last row unset.
Tracing an example
This is the finished table for "horse" and "ros". Rows are prefixes of horse (0 to 5 characters), columns are prefixes of ros (0 to 3):
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 bottom right entry, 3, is the answer. Look at row 2, column 2: the prefixes ho and ro both end in o, so the entry copies the diagonal entry, dp[1][1] = 1, the cost of turning h into r. In row 1, column 1, the last characters h and r differ, so the entry is 1 plus the smallest of its three neighbours, which are all 0 or 1 there: 1 + 0 = 1.
Complexity
Time: there are (m + 1) × (n + 1) entries and each takes constant work: O(m · n). With words of 500 characters that is about 250,000 entries.
Space: the whole table is O(m · n). Each row only reads itself and the row above, so two rows of n + 1 numbers would be enough for O(n), but keeping the whole table is easier to follow.
Edge cases and common mistakes
- Empty words.
minDistance("", "abc")is 3 andminDistance("abc", "")is 3. Two empty words give 0. - Identical words. The diagonal copies carry 0 all the way, so
minDistance("abc", "abc")is 0. - Off-by-one in the indexes. Comparing
word1[i]withword2[j]reads the wrong characters and, ati == m, fails with an index error. The prefix of lengthiends atword1[i - 1]. - Forgetting the borders. Without them every entry on the edge stays 0, and the answer is too small.
- Forgetting the 1. In the different-characters case the operation itself costs 1.
- Swapped letters.
minDistance("ab", "ba")is 2: this problem has no swap operation, so it takes two replaces.
Testing
minDistance("horse", "ros") = 3
minDistance("intention", "execution") = 5
minDistance("", "abc") = 3
minDistance("kitten", "sitting") = 3
minDistance("ab", "ba") = 2
