LeetCode - 150 - Edit Distance
The problem
Given two strings word1 and word2, return the minimum number of operations required to convert word1 to word2.
You have the following three operations permitted on a word:
-
Insert a character
-
Delete a character
-
Replace a character
Example 1:
Input: word1 = "horse", word2 = "ros"
Output: 3
Explanation:
horse -> rorse (replace 'h' with 'r')
rorse -> rose (remove 'r')
rose -> ros (remove 'e')
Example 2:
Input: word1 = "intention", word2 = "execution"
Output: 5
Explanation:
intention -> inention (remove 't')
inention -> enention (replace 'i' with 'e')
enention -> exention (replace 'n' with 'x')
exention -> exection (replace 'n' with 'c')
exection -> execution (insert 'u')
Constraints:
0 <= word1.length, word2.length <= 500word1andword2consist of lowercase English letters.
Recursion Solution
Explanation
With the problem description in mind, let’s go through the base cases that we can get from our constraints:
- one of the base cases is when both
word1andword2are empty strings. In this case, it would take0operations to convertword1toword2. - the next base case would be if we had the same strings in
word1andword2, for example strings likeabc. In this case, it would also take0operations to convertword1toword2. - the next case would be if we had characters in
word1but an empty string inword2. For exampleword1 = "abc",word2 = "". In this case, we would need to remove all characters fromword1and return the result3. - in the case where we have an empty string in
word1but characters inword2, for exampleword1 = "",word2 = "abc", we would need to insert all characters fromword2intoword1, which would result in3operations.
if one of
word1orword2is empty, we would return the length of the non-empty string.
when both word1 and word2 are not empty, we are going to compare them character by character. For example, let’s imagine that we have word1 = "acd" and word2 = "abd". We are also going to have pointers i and j that will help us track the current character in each word.
- if you look at the example, you can see that the first characters in both words are equal, therefore we are going to increment both pointers
iandjby one:(i + 1, j + 1). We did not perform any operations here, therefore the operations count stays0.
when we move to the next characters, you can see that they are not equal. This means that we can apply insert, delete, replace operations:
- if we try to
insertcharacterbintoword1, we would need to increase the operations count by one, increment thejpointer by one, and leave theipointer in the same position. - if we try to
deletetheccharacter inword1, we would need to increase the operations count, increment theipointer, and leave thejpointer in the same position. - lastly, if we try to
replacetheccharacter inword1with thebcharacter fromword2so they match, we would need to increase the operations count and increment bothiandjpointers.
Now that we know all base cases, we can apply that knowledge and develop an algorithm that is going to solve this problem.
func minDistance(_ word1: String, _ word2: String) -> Int {
let m = word1.count
let n = word2.count
let word1Arr = Array(word1)
let word2Arr = Array(word2)
func dfs(_ i: Int, _ j: Int) -> Int {
if i == m {
return n - j
}
if j == n {
return m - i
}
if word1Arr[i] == word2Arr[j] {
return dfs(i + 1, j + 1)
}
var res = min(dfs(i + 1, j), dfs(i, j + 1))
res = min(res, dfs(i + 1, j + 1))
return res + 1
}
return dfs(0, 0)
}
Time/ Space complexity
- Time complexity: O(3^(m + n))
- Space complexity: O(m + n)
- Where
mis the length ofword1andnis the length ofword2
Dynamic Programming (Top-Down) Solution
Explanation
If you look at the time complexity of the recursive solution, you can see that it is not very efficient. Furthermore, it won’t pass on LeetCode. When we are using a recursive solution, we visit the same i, j positions multiple times, so to avoid that we are going to use caching that will store already calculated results in a dictionary.
func minDistance(_ word1: String, _ word2: String) -> Int {
let m = word1.count
let n = word2.count
let word1Arr = Array(word1)
let word2Arr = Array(word2)
struct Key: Hashable {
let i: Int
let j: Int
}
var cache: [Key: Int] = [:]
func dfs(_ i: Int, _ j: Int) -> Int {
if i == m {
return n - j
}
if j == n {
return m - i
}
if let cached = cache[Key(i: i, j: j)] {
return cached
}
if word1Arr[i] == word2Arr[j] {
cache[Key(i: i, j: j)] = dfs(i + 1, j + 1)
} else {
var res = min(dfs(i + 1, j), dfs(i, j + 1))
res = min(res, dfs(i + 1, j + 1))
cache[Key(i: i, j: j)] = res + 1
}
return cache[Key(i: i, j: j)]!
}
return dfs(0, 0)
}
Time/ Space complexity
- Time complexity: O(m * n)
- Space complexity: O(m * n)
- Where
mis the length ofword1andnis the length ofword2
Dynamic Programming (Bottom-Up) Solution
Explanation
So far in our solutions, we used recursion, but in order to solve this problem in a true dynamic programming way, we are going to create a 2D grid where we will store the minimum number of operations to make substrings equal. We are also going to add an additional row and column to our grid—this is going to help us work with the base case when we have an empty string in one or both of our words.
let’s look at base cases:
- If both words are empty, it will take
0operations because both strings are equal. - If we were calculating the operations count for a column with an empty string, we would calculate the length of
word1at the current row and put the value in the current cell. For example, for the first row and the last column, it will takeword1.count = 3operations; for the second row2, etc. - The same logic applies for the last row, but instead of calculating
word1count, we are going to calculateword2count. For example, for the first row and first column, the operations count is going to beword2.count = 3, for the second column2, etc.
Since we have three choices where we can move—to the right, below, and diagonally—we are going to iterate from bottom up and find the minimum number of operations for each cell. Then, when the iteration is complete, we are going to return the result from the first row and column.
For example, for row d and column d, we are going to put 0 in the cell because they are both equal and it took us 0 operations. For the row d and column c, even though our minimum is going to be 0, we are going to add 1 to it because those values are not equal.
func minDistance(_ word1: String, _ word2: String) -> Int {
let word1Arr = Array(word1)
let word2Arr = Array(word2)
var dp = Array(repeating: Array(repeating: Int.max, count: word2.count + 1), count: word1.count + 1)
for i in 0 ..< word1.count + 1 {
dp[i][word2.count] = word1.count - i
}
for j in 0 ..< word2.count + 1 {
dp[word1.count][j] = word2.count - j
}
for i in stride(from: word1.count - 1, to: -1, by: -1) {
for j in stride(from: word2.count - 1, to: -1, by: -1) {
if word1Arr[i] == word2Arr[j] {
dp[i][j] = dp[i + 1][j + 1]
} else {
dp[i][j] = 1 + min(dp[i + 1][j], dp[i][j + 1], dp[i + 1][j + 1])
}
}
}
return dp[0][0]
}
Time/ Space complexity
- Time complexity: O(m * n)
- Space complexity: O(m * n)
- Where
mis the length ofword1andnis the length ofword2