Problem overview
The function minDistance(word1, 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. Both words are lowercase letters, up to 500 characters each, and either may be empty.
For word1 = "horse" and word2 = "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, and for "" and "abc" it is 3, three inserts.
Approach: solve every pair of prefixes
Trying every sequence of operations is hopeless, since the number of sequences explodes with the word lengths. The way in is to solve smaller versions of the same problem. 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 = len(word1) and n = len(word2), the table has (m + 1) × (n + 1) entries and the answer is dp[m][n]. The extra row and column stand for the empty prefix.
The edges of the table are easy:
dp[i][0] = i: turningicharacters into nothing takesideletes.dp[0][j] = j: turning nothing intojcharacters takesjinserts.
The rest is filled row by row, left to right, so the entries above, to the left and diagonally up-left of dp[i][j] are known when it is reached. The decision depends on 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 result is 1 plus the best of three neighbours:
dp[i - 1][j - 1]for a replace, after which both prefixes are one character shorter;dp[i - 1][j]for a delete ofword1's last character;dp[i][j - 1]for an insert ofword2's last character at the end, which then matches, so onlyword2's prefix gets shorter.
For "horse" and "ros" the finished table is below. Rows are the prefixes of "horse" (the empty one first) and 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.
m = len(word1)andn = len(word2), one per line, since both come up again and again. - Build
dpwith a list comprehension.[[0] * (n + 1) for _ in range(m + 1)]creates a new row ofn + 1zeros for each of them + 1rows. The name_says the loop variable isn't used. - Loop
ioverrange(m + 1)and setdp[i][0] = i. This fills column 0, the empty prefix ofword2. It runs from 0 tom, since every row has an entry in that column. - Loop
joverrange(n + 1)and setdp[0][j] = j. This fills row 0 the same way. The cornerdp[0][0]is set twice, both times to 0. - Loop
ioverrange(1, m + 1). Row 0 is done, so the loop starts at 1, andrangestops before its end, hencem + 1. - Loop
joverrange(1, n + 1)inside it. Column 0 is already filled, sojstarts at 1. - If
word1[i - 1] == word2[j - 1], copy the diagonal. The firsticharacters end at indexi - 1: the word counts from 0, while the table counts characters. - Otherwise take 1 plus the
minof the three neighbours.minaccepts any number of arguments, somin(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1])covers replace, delete and insert. The 1 counts the operation itself. - Return
dp[m][n]. It covers all of both words.
The solution in Python
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
m = len(word1)
n = 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:
dp[i][j] = 1 + min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1])
return dp[m][n]
Python details that matter here:
- Build the rows separately.
[[0] * (n + 1)] * (m + 1)looks equivalent, but it repeats one row objectm + 1times. Assigning to one cell then shows up in every row. With a 3 by 3 table made that way, settingbad[0][1] = 9gives[[0, 9, 0], [0, 9, 0], [0, 9, 0]]. The comprehension runs[0] * (n + 1)once per row, so each row is its own list. - Strings index from 0.
word1[i - 1]is thei-th character. Strings are immutable, so the code never changes the words; it only reads them. - Empty words are fine.
len("")is 0, so the loops overrange(1, m + 1)orrange(1, n + 1)are empty and the answer comes straight from the edge of the table.
Complexity
The table has (m + 1) × (n + 1) entries, and each one takes constant work, so the time is O(m · n) and the space is O(m · n). Each row reads only itself and the row above, so keeping two rows would bring the space down to O(n). The lesson keeps the whole table because it is easier to follow, and because the table can be printed and checked by hand.
Edge cases and common mistakes
- One empty word.
minDistance("", "abc")andminDistance("abc", "")are both 3, and two empty words give 0. The edges of the table handle all of these. - Identical words.
minDistance("same", "same")is 0: every pair of last characters matches, so the diagonal carries 0 to the end. - Off-by-one with the indexes. The table entry
dp[i][j]is abouticharacters, so the characters to compare are ati - 1andj - 1. Comparingword1[i]withword2[j]reads the wrong characters and fails at the last index. - Forgetting the +1. When the characters differ, taking the
minof the neighbours alone would count the fix as free. - Treating a swap as one operation. There is no swap:
"ab"to"ba"costs 2, two replaces. - Filling in the wrong order. Row by row with
jincreasing guarantees that the three neighbours exist. Filling column by column works as well, but mixing the directions does not.
Testing
minDistance("horse", "ros") -> 3
minDistance("intention", "execution") -> 5
minDistance("", "abc") -> 3
minDistance("abc", "") -> 3
minDistance("", "") -> 0
minDistance("ab", "ba") -> 2
minDistance("kitten", "sitting") -> 3
