Problem overview
The method minDistance receives two strings, word1 and word2, and returns the fewest operations that turn word1 into word2. The operations are inserting a character anywhere, deleting a character, and replacing a character with another one. Each costs 1. Both words are lowercase letters and either may be empty.
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. Two operations cannot work, because "ros" is two characters shorter, so both would have to be deletes, and deleting letters from "horse" never gives "ros". For "" and "abc" the answer is 3, three inserts.
Approach: solve every pair of prefixes
Trying every sequence of operations is far too slow. The method solves smaller versions of the problem instead. 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).
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.
Every other entry is filled from three neighbours that are already known, by looking 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 fixes the end, and the entry is 1 plus the smallest of three neighbours.
dp(i - 1)(j - 1)is a replace: both prefixes lose their last character.dp(i - 1)(j)is a delete ofword1's character.dp(i)(j - 1)is an insert ofword2's character at the end, which matches the last character ofword2's prefix.
This is the full table for "horse" and "ros". Rows are the prefixes of horse (including the empty one), columns the prefixes of ros:
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.
Step by step
- Store the lengths of the words in
mandn, onevaleach. They come up in the table size and in every loop. - Create the table
dpwithArray.ofDim[Int](m + 1, n + 1), which makesm + 1rows ofn + 1zeros. The extra row and column are the empty prefixes. - Fill column 0 with a loop over
i <- 0 to m, settingdp(i)(0)toi. Both ends are included, because every row has an entry in column 0. - Fill row 0 with a loop over
j <- 0 to n, settingdp(0)(j)toj. The cornerdp(0)(0)is set twice, both times to 0. - Loop over the rest of the table row by row with
i <- 1 to mand, inside it,j <- 1 to n. Row 0 and column 0 are already done, so both loops start at 1. In this order the entries above, to the left and diagonally up-left are filled before they are needed. - Handle equal characters. Compare
word1(i - 1)withword2(j - 1). A prefix of lengthiends at indexi - 1, because a word counts from 0 and the table counts characters. If they are equal, copydp(i - 1)(j - 1). - Handle different characters in the
else. Set the entry to 1 plus the minimum of the replace, delete and insert neighbours. Twomath.mincalls are nested, since each compares just two values. - Return
dp(m)(n)as the last expression of the method.
The solution in Scala
class Solution {
def minDistance(word1: String, word2: String): Int = {
val m = word1.length
val n = word2.length
val dp = Array.ofDim[Int](m + 1, n + 1)
for (i <- 0 to m) {
dp(i)(0) = i
}
for (j <- 0 to n) {
dp(0)(j) = j
}
for (i <- 1 to m) {
for (j <- 1 to n) {
if (word1(i - 1) == word2(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)))
}
}
}
dp(m)(n)
}
}
The Scala details worth a closer look:
Array.ofDim[Int](m + 1, n + 1)creates an array of arrays, with every element 0. ForArray.ofDim[Int](2, 3)the rows print as0,0,0 | 0,0,0. Its type isArray[Array[Int]].- One pair of parentheses per dimension. An element is
dp(i)(j):dp(i)is a row, and the second pair picks the column. Assignment looks the same,dp(i)(0) = i. - Strings are indexed like arrays.
word1(i - 1)is theCharat that index, and==compares twoChars by value. - Empty words are safe. If
word1is empty,mis 0, and1 to mis an empty range, so the main loops do nothing anddp(0)(n)isn. The same holds for an emptyword2. math.mintakes two arguments. To find the smallest of three values, nest two calls, as the lesson does.toversusuntil.0 to mincludesm, which the table needs, since it hasm + 1rows.0 until mwould stop one row short.
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. Each row reads only itself and the row above it, so two rows of n + 1 numbers would be enough and would lower the space to O(n). The full table is easier to follow.
Edge cases and common mistakes
- An empty word.
minDistance("", "abc")is 3 andminDistance("abc", "")is also 3. Two empty words give 0. - Equal words.
minDistance("same", "same")is 0, because the diagonal copies zeros all the way down. - Swapped letters.
minDistance("ab", "ba")is 2: there is no swap operation, so two replaces are needed. Its table is:0 1 2 1 1 1 2 1 2 - One word is a prefix of the other.
minDistance("abc", "ab")is 1, a single delete. - Using
iinstead ofi - 1to read the characters. The table has one more row and column than the words have characters, soword1(i)reads the wrong letter, and fori = mit is out of range. - Forgetting the
+ 1for the operation. Without it, the entry would just copy the cheapest neighbour, and differences between the words would cost nothing. - Skipping the first row and column. Without the base cases, every entry depends on zeros that should have been
iandj, and all answers come out too small.
Testing
"horse"and"ros"give3."intention"and"execution"give5.""and"abc"give3."abc"and""give3."a"and"b"give1.- Two strings of 500 letters each,
arepeated againstbrepeated, give500.
