LeetCode热题(JS版)- 72. 编辑距离

194 阅读2分钟

题目

给你两个单词 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,这也符合动态规划的初始条件。

image.png

  • 时间复杂度为 O(mn),其中 m 和 n 分别为两个单词的长度。
  • 空间复杂度为 O(mn),需要使用一个二维数组来记录状态。