72. 编辑距离 (edit distance)

3,922 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第26天,点击查看活动详情

72. 编辑距离 题目描述:给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数。你可以对一个单词进行如下三种操作:

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符
示例1示例2
输入word1 = "horse", word2 = "ros"
输出33
解释
11步: horse -> rorse (将 'h' 替换为 'r')
22步: rorse -> rose (删除 'r')
33步: rose -> ros (删除 'e')
输入word1 = "intention", word2 = "execution"
输出55
解释
11步: intention -> inention (删除 't')
22步: inention -> enention (将 'i' 替换为 'e')
33步: enention -> exention (将 'n' 替换为 'x')
44步: exention -> exection (将 'n' 替换为 'c')
55步: exection -> execution (插入 'u')

中规中矩的动态规划

这一道是完全意义上的编辑距离问题。之前已经更新过三篇入门级别的有关编辑距离问题的题解,

1、确定 dp 状态数组

定义 dp[i][j]dp[i][j] 是以 i1i - 1 结尾的 word1 子序列和以 j1j - 1 结尾的 word2 子序列的最小编辑距离,其中 i[1,len1],j[1,len2]i \in [1, len_1], j \in [1, len_2]len1=word1.length,len2=word2.lengthlen_1=word1.length,len_2 = word2.length

2、确定 dp 状态方程

判断 word1[i1]word1[i - 1]word2[j1]word2[j - 1] 的关系:

word1[i1]==word2[j1]word1[i - 1] == word2[j - 1] 时,以 i1i - 1 结尾的 word1 子序列和以 j1j - 1 结尾的 word2 子序列的最小编辑距离,即 dp[i][j]dp[i][j],应等于以 i2i - 2 结尾的 word1 子序列和以 j2j - 2 结尾的 word2 子序列的最小编辑距离,即 dp[i1][j1]dp[i - 1][j - 1]

dp[i][j]=dp[i1][j1]dp[i][j] = dp[i - 1][j - 1]

word1[i1]!=word2[j1]word1[i - 1] != word2[j - 1] 时,此时可以选择执行插入、删除、替换三种操作之一:

  1. 如果删除一个元素(从 word1 的子序删除),以 i1i - 1 结尾的 word1 子序列和以 j1j - 1 结尾的 word2 子序列的最小编辑距离,即 dp[i][j]dp[i][j],应比 i2i - 2 结尾的 word1 子序列和以 j1j - 1 结尾的 word2 子序列的最小编辑距离,即 dp[i1][j]dp[i - 1][j],多 11,此时 dp[i][j]=dp[i1][j]+1dp[i][j] = dp[i - 1][j] + 1

  2. 同理,如果删除一个元素(从 word2 的子序删除),以 i1i - 1 结尾的 word1 子序列和以 j1j - 1 结尾的 word2 子序列的最小编辑距离,即 dp[i][j]dp[i][j],应比 i1i - 1 结尾的 word1 子序列和以 j2j - 2 结尾的 word2 子序列的最小编辑距离,即 dp[i][j1]dp[i][j - 1],多 11,此时 dp[i][j]=dp[i][j1]+1dp[i][j] = dp[i][j - 1] + 1

  3. 如果增加一个元素(从 word1 的子序增加),以 i1i - 1 结尾的 word1 子序列和以 j1j - 1 结尾的 word2 子序列的最小编辑距离,即 dp[i][j]dp[i][j],应比 i1i - 1 结尾的 word1 子序列和以 j2j - 2 结尾的 word2 子序列的最小编辑距离,即 dp[i][j1]dp[i][j - 1],多 11,此时 dp[i][j]=dp[i][j1]+1dp[i][j] = dp[i][j - 1] + 1(从 word1 增加元素就如同从 word2 删除元素)

  4. 同理,如果增加一个元素(从 word2 的子序增加),以 i1i - 1 结尾的 word1 子序列和以 j1j - 1 结尾的 word2 子序列的最小编辑距离,即 dp[i][j]dp[i][j],应比 i2i - 2 结尾的 word1 子序列和以 j1j - 1 结尾的 word2 子序列的最小编辑距离,即 dp[i1][j]dp[i - 1][j],多 11,此时 dp[i][j]=dp[i1][j]+1dp[i][j] = dp[i - 1][j] + 1(从 word2 增加元素就如同从 word1 删除元素)

  5. 替换元素,此时不需要增加/删除元素,以 i1i - 1 结尾的 word1 子序列和以 j1j - 1 结尾的 word2 子序列的最小编辑距离,即 dp[i][j]dp[i][j],应比 i2i - 2 结尾的 word1 子序列和以 j2j - 2 结尾的 word2 子序列的最小编辑距离,即 dp[i1][j]dp[i - 1][j],多 11,此时 dp[i][j]=dp[i1][j1]+1dp[i][j] = dp[i - 1][j - 1] + 1

综上所述,word1[i1]!=word2[j1]word1[i - 1] != word2[j - 1] 时,

dp[i][j]=min(dp[i1][j1],dp[i1][j],dp[i][j1])+1dp[i][j]= min(dp[i-1][j-1],dp[i-1][j],dp[i][j - 1]) + 1

3、确定 dp 初始状态

  1. 声明规模为 (len1+1)×(len2+1)(len_1 + 1) \times (len_2 + 1),且元素均为 00 的数组;

  2. dp[i][0]dp[i][0] 为以 i1i - 1 结尾的 word1 子序列和空字符串的最小编辑距离,即 d[i][0]=id[i][0] = i

  3. dp[0][j]dp[0][j] 为空字符串和以 j1j - 1 结尾的 word2 子序列的最小编辑距离,即 d[0][j]=jd[0][j] = j

4、确定遍历顺序

  • 外层循环遍历 word1,从 i=1i = 1i=len1i = len_1

  • 内层循环遍历 word2,从 j=1j = 1j=len2j = len_2

5、确定最终返回值

回归到状态定义中,即 dp[len1][len2]dp[len_1][len_2]

6、代码示例

/**
 * 空间复杂度 O(word1.length * word2.length)
 * 时间复杂度 O(word1.length * word2.length)
 */
 function minDistance(word1: string, word2: string): number {
    const len1 = word1.length;
    const len2 = word2.length;
    const dp = Array.from({ length: len1 + 1 }, () => new Array(len2 + 1).fill(0));

    for (let i = 0; i <= len1; i++) {
        dp[i][0] = i;
    }

    for (let j = 0; j <= len2; j++) {
        dp[0][j] = j;
    }

    for (let i = 1; i <= len1; i++) {
        for (let j = 1; j <= len2; 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[len1][len2];
};

参考

# 重识动态规划