DAY44

92 阅读7分钟

第九章 动态规划part12

115.不同的子序列

但相对于刚讲过 392.判断子序列,本题 就有难度了 ,感受一下本题和 392.判断子序列 的区别。

programmercarl.com/0115.%E4%B8…

if (s[i - 1] == t[j - 1]) {
    dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
} else {
    dp[i][j] = dp[i - 1][j];
}
动态规划数组 dp[i][j] 的含义
  • dp[i][j] 表示使用 s 的前 i 个字符可以匹配 t 的前 j 个字符的不同子序列的数量。

边界条件

  • dp[i][0] = 1:当 t 为空字符串时,任何字符串 s 都有 1 个子序列匹配空字符串(即不选任何字符)。
  • dp[0][j] = 0:当 s 为空而 t 非空时,无法通过 s 匹配 t,所以 dp[0][j] = 0
DP 递推公式解释
  • s[i - 1] == t[j - 1] 时:

    • dp[i - 1][j - 1] 表示前 i - 1 个字符的 s 子序列与前 j - 1 个字符的 t 匹配的数量。
    • dp[i - 1][j] 表示前 i - 1 个字符的 s 子序列与前 j 个字符的 t 匹配的数量,即我们可以选择跳过当前字符 s[i - 1] 而继续匹配。

    因此,如果 s[i - 1] == t[j - 1],我们有两种选择:

    1. 匹配掉 s[i - 1]t[j - 1],然后继续用 s 的前 i - 1 个字符去匹配 t 的前 j - 1 个字符(dp[i - 1][j - 1])。
    2. 不匹配 s[i - 1]t[j - 1],而是跳过 s[i - 1],继续用 s 的前 i - 1 个字符去匹配 t 的前 j 个字符(dp[i - 1][j])。

    因此,递推公式为: [ dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j] ]

  • s[i - 1] != t[j - 1] 时:

    • 在这种情况下,s[i - 1] 不能匹配 t[j - 1],所以我们只能跳过 s[i - 1],即我们只能用 s 的前 i - 1 个字符去匹配 t 的前 j 个字符。因此: [ dp[i][j] = dp[i - 1][j] ]
直观理解
  • 如果 s[i - 1] == t[j - 1],意味着我们有两种方式来匹配:

    1. s[i - 1]t[j - 1] 对应起来,这样就减少了一个待匹配的字符。
    2. 不使用 s[i - 1],继续用前面的字符去匹配。

    因此,匹配的总方式是两者的和。

  • 如果 s[i - 1] != t[j - 1],则只能选择跳过 s[i - 1]

示例

考虑示例: S = "rabbbit", T = "rabbit"

初始化 dp 数组:

''rabbit
''1000000
r1
a1
b1
b1
b1
i1
t1

根据递推公式一步步计算出最终结果。

583. 两个字符串的删除操作

本题和动态规划:115.不同的子序列 相比,其实就是两个字符串都可以删除了,情况虽说复杂一些,但整体思路是不变的。

programmercarl.com/0583.%E4%B8…

动态规划数组 dp[i][j] 的含义
  • dp[i][j] 表示将 word1 的前 i 个字符和 word2 的前 j 个字符通过最少的删除次数变成相同字符串所需要的最少删除次数。
递推公式

为了得到 dp[i][j] 的值,我们需要比较 word1[i-1]word2[j-1]

  1. word1[i - 1] === word2[j - 1]

    • 如果两个字符相同,则不需要进行任何删除操作,可以继承前一个状态: [ dp[i][j] = dp[i - 1][j - 1] ] 因为这两个字符相等,可以不进行删除,直接看前面的状态。
  2. word1[i - 1] !== word2[j - 1]

    • 在这种情况下,我们有三种可能的删除操作,选择代价最小的一种:
      1. 删除 word1[i - 1]:从 dp[i - 1][j] 继承过来,并增加一次删除操作,操作次数为 dp[i - 1][j] + 1
      2. 删除 word2[j - 1]:从 dp[i][j - 1] 继承过来,并增加一次删除操作,操作次数为 dp[i][j - 1] + 1
      3. 同时删除 word1[i - 1]word2[j - 1]:从 dp[i - 1][j - 1] 继承过来,并增加两次删除操作,操作次数为 dp[i - 1][j - 1] + 2

    综合这三种删除操作,取最小值: [ dp[i][j] = \min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + 2) ]

边界条件
  • i == 0 时,表示 word1 为空,最少删除次数就是删除 word2 的所有字符,因此 dp[0][j] = j
  • j == 0 时,表示 word2 为空,最少删除次数就是删除 word1 的所有字符,因此 dp[i][0] = i
代码实现
/**
 * @param {string} word1
 * @param {string} word2
 * @return {number}
 */
var minDistance = function(word1, word2) {
    let m = word1.length, n = word2.length;
    let dp = Array(m + 1).fill(0).map(() => Array(n + 1).fill(0));

    // 初始化边界条件
    for (let i = 0; i <= m; i++) {
        dp[i][0] = i;  // 删除 word1 的所有字符
    }
    for (let j = 0; j <= n; j++) {
        dp[0][j] = j;  // 删除 word2 的所有字符
    }

    for (let i = 1; i <= m; i++) {
        for (let j = 1; j <= n; 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,       // 删除 word1[i-1]
                    dp[i][j - 1] + 1,       // 删除 word2[j-1]
                    dp[i - 1][j - 1] + 2    // 删除 word1[i-1] 和 word2[j-1]
                );
            }
        }
    }

    return dp[m][n];
};
示例

如果 word1 = "sea"word2 = "eat",DP 表格如下:

''eat
''0123
s1234
e2123
a3212

最终结果 dp[3][3] = 2,表示将 sea 变成 eat 需要 2 次删除操作。

总结

dp[i][j] 的递推关系中,当 word1[i - 1] === word2[j - 1] 时,两个字符相同,不需要删除,否则会比较三种删除操作的最小值。

72. 编辑距离

最终我们迎来了编辑距离这道题目,之前安排题目都是为了 编辑距离做铺垫。

programmercarl.com/0072.%E7%BC…

动态规划数组 dp[i][j] 的含义
  • dp[i][j] 表示将 word1 的前 i 个字符转换成 word2 的前 j 个字符所需的最少操作次数。
递推公式

为了得到 dp[i][j] 的值,我们需要比较 word1[i-1]word2[j-1]

  1. word1[i - 1] == word2[j - 1]

    • 这意味着这两个字符是相同的,因此不需要进行任何操作,我们可以直接继承前一个状态: [ dp[i][j] = dp[i - 1][j - 1] ] 即从 dp[i-1][j-1] 继承过来。
  2. word1[i - 1] != word2[j - 1]

    • 在这种情况下,当前字符不同,我们有三种操作可以选择:
      1. 替换 word1[i-1]word2[j-1],然后将前面的部分匹配上,操作次数为 dp[i - 1][j - 1] + 1
      2. 删除 word1[i-1],然后继续将 word1 的前 i-1 个字符匹配到 word2 的前 j 个字符,操作次数为 dp[i - 1][j] + 1
      3. 插入 word2[j-1]word1,然后将 word1 的前 i 个字符匹配到 word2 的前 j-1 个字符,操作次数为 dp[i][j - 1] + 1

    综合这三种操作,我们取最小值: [ dp[i][j] = \min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]) + 1 ]

边界条件
  • i == 0 时,即 word1 为空,最少操作次数为插入 word2 的所有字符,dp[0][j] = j
  • j == 0 时,即 word2 为空,最少操作次数为删除 word1 的所有字符,dp[i][0] = i
代码实现
/**
 * @param {string} word1
 * @param {string} word2
 * @return {number}
 */
var minDistance = function(word1, word2) {
    let m = word1.length, n = word2.length;
    let dp = Array(m + 1).fill(0).map(() => Array(n + 1).fill(0));

    // 初始化边界条件
    for (let i = 0; i <= m; i++) {
        dp[i][0] = i;  // 删除操作
    }
    for (let j = 0; j <= n; j++) {
        dp[0][j] = j;  // 插入操作
    }

    for (let i = 1; i <= m; i++) {
        for (let j = 1; j <= n; 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 - 1][j],      // 删除操作
                    dp[i][j - 1]       // 插入操作
                ) + 1;
            }
        }
    }

    return dp[m][n];
};
示例

如果 word1 = "horse"word2 = "ros",那么 DP 表格会填充如下:

''ros
''0123
h1123
o2212
r3222
s4332
e5443

最终结果 dp[5][3] = 3,表示将 horse 变成 ros 需要 3 次操作。

编辑距离总结篇

做一个总结吧

programmercarl.com/%E4%B8%BA%E…