题目
给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符 删除一个字符 替换一个字符
示例 1:
输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
示例 2:
输入:word1 = "intention", word2 = "execution"
输出:5
解释:
intention -> inention (删除 't')
inention -> enention (将 'i' 替换为 'e')
enention -> exention (将 'n' 替换为 'x')
exention -> exection (将 'n' 替换为 'c')
exection -> execution (插入 'u')
提示:
0 <= word1.length, word2.length <= 500
word1 和 word2 由小写英文字母组成
思路
本题是一道经典的动态规划题目,我们可以使用两个指针 i 和 j 分别指向两个单词的末尾,然后从末尾开始往前进行比较。假设 word1 的长度为 m,word2 的长度为 n,则我们可以定义一个二维数组 dp,其中 dp[i][j] 表示将 word1 的前 i 个字符转换成 word2 的前 j 个字符所需要的最少操作数。 那么,我们可以考虑当 word1[i] === word2[j] 时,此时不需要进行任何操作,因此 dp[i][j] = dp[i - 1][j - 1]。如果 word1[i] !== word2[j],那么我们有三种操作可以选择,分别是插入、删除、替换操作:
- 插入操作:我们可以将 word2 的第 j 个字符插入到 word1 的第 i 个字符的位置上,此时需要进行一次插入操作,因此 dp[i][j] = dp[i][j - 1] + 1。
- 删除操作:我们可以将 word1 的第 i 个字符删除,此时需要进行一次删除操作,因此 dp[i][j] = dp[i - 1][j] + 1。
- 替换操作:我们可以将 word1 的第 i 个字符替换成 word2 的第 j 个字符,此时需要进行一次替换操作,因此 dp[i][j] = dp[i - 1][j - 1] + 1。 最终的答案即为 dp[m][n]。
function minDistance(word1: string, word2: string): number {
const n = word1.length;
const m = word2.length;
// m + 1,n + 1是为了计算长度,并且最初的设置为0避免越界
const dp = new Array(n + 1).fill(null).map(d => new Array(m + 1).fill(0));
// word1 -> 空
for(let i = 0; i <= n; i++) {
dp[i][0] = i;
}
// 空 -> word2
for(let j = 0; j <= m; j++) {
dp[0][j] = j;
}
// 动态规划,考虑4种情况
// 插入:dp[i][j-1] + 1。将 word2 的第 j 个字符插入到 word1 的第 i 个字符的位置上
// 删除:dp[i-1][j] + 1。将 word1 的第 i 个字符删除
// 交换:dp[i-1][j-1] + 1。将 word1 的第 i 个字符替换成 word2 的第 j 个字符
// 相等:不变
for(let i = 1; i <= n; i++) {
console.log(i)
for(let j = 1; j <= m; j++) {
if(word1[i - 1] === word2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = Math.min(dp[i - 1][j - 1], dp[i][j - 1], dp[i - 1][j]) + 1;
}
}
}
return dp[n][m];
};
解释下 dp[i][0] = i
当 word2 为空字符串时,无论 word1 是什么,将它转换成空字符串的最小操作数都是它本身的长度。因为此时只需要将 word1 中的所有字符都删除即可。
因此,我们可以初始化 dp[i][0] = i,表示将 word1 的前 i 个字符全部删除所需要的最小操作数为 i。
解释下m+1,n+1
在实现动态规划时,我们通常会定义一个二维数组来记录状态。
对于本题来说,我们可以定义一个二维数组 dp,其中 dp[i][j] 表示将 word1 的前 i 个字符转换成 word2 的前 j 个字符所需要的最少操作数。
但是需要注意的是,我们在定义一个数组时,其下标是从 0 开始的。
而在本题中,我们需要记录 word1 和 word2 的所有前缀的最少操作数,因此需要将数组 dp 的行数和列数都加 1,即 dp 的大小为 (m + 1) * (n + 1)。
这样做的好处是,可以避免边界问题。例如,在计算 dp[i][j] 的时候,如果 i 或 j 等于 0,那么 dp[i - 1][j - 1] 就会越界。
而如果将 dp 的大小增加 1,就可以避免这个问题的出现。
同时,dp[0][0] 表示空字符串转换成空字符串所需要的最小操作数,即为 0,这也符合动态规划的初始条件。
- 时间复杂度为 O(mn),其中 m 和 n 分别为两个单词的长度。
- 空间复杂度为 O(mn),需要使用一个二维数组来记录状态。