第九章 动态规划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],我们有两种选择:- 匹配掉
s[i - 1]和t[j - 1],然后继续用s的前i - 1个字符去匹配t的前j - 1个字符(dp[i - 1][j - 1])。 - 不匹配
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],意味着我们有两种方式来匹配:- 将
s[i - 1]与t[j - 1]对应起来,这样就减少了一个待匹配的字符。 - 不使用
s[i - 1],继续用前面的字符去匹配。
因此,匹配的总方式是两者的和。
- 将
-
如果
s[i - 1] != t[j - 1],则只能选择跳过s[i - 1]。
示例
考虑示例:
S = "rabbbit", T = "rabbit"。
初始化 dp 数组:
| '' | r | a | b | b | i | t | |
|---|---|---|---|---|---|---|---|
| '' | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
| r | 1 | ||||||
| a | 1 | ||||||
| b | 1 | ||||||
| b | 1 | ||||||
| b | 1 | ||||||
| i | 1 | ||||||
| t | 1 |
根据递推公式一步步计算出最终结果。
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]:
-
当
word1[i - 1] === word2[j - 1]时:- 如果两个字符相同,则不需要进行任何删除操作,可以继承前一个状态: [ dp[i][j] = dp[i - 1][j - 1] ] 因为这两个字符相等,可以不进行删除,直接看前面的状态。
-
当
word1[i - 1] !== word2[j - 1]时:- 在这种情况下,我们有三种可能的删除操作,选择代价最小的一种:
- 删除
word1[i - 1]:从dp[i - 1][j]继承过来,并增加一次删除操作,操作次数为dp[i - 1][j] + 1。 - 删除
word2[j - 1]:从dp[i][j - 1]继承过来,并增加一次删除操作,操作次数为dp[i][j - 1] + 1。 - 同时删除
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 表格如下:
| '' | e | a | t | |
|---|---|---|---|---|
| '' | 0 | 1 | 2 | 3 |
| s | 1 | 2 | 3 | 4 |
| e | 2 | 1 | 2 | 3 |
| a | 3 | 2 | 1 | 2 |
最终结果 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]:
-
当
word1[i - 1] == word2[j - 1]时:- 这意味着这两个字符是相同的,因此不需要进行任何操作,我们可以直接继承前一个状态:
[
dp[i][j] = dp[i - 1][j - 1]
]
即从
dp[i-1][j-1]继承过来。
- 这意味着这两个字符是相同的,因此不需要进行任何操作,我们可以直接继承前一个状态:
[
dp[i][j] = dp[i - 1][j - 1]
]
即从
-
当
word1[i - 1] != word2[j - 1]时:- 在这种情况下,当前字符不同,我们有三种操作可以选择:
- 替换
word1[i-1]为word2[j-1],然后将前面的部分匹配上,操作次数为dp[i - 1][j - 1] + 1。 - 删除
word1[i-1],然后继续将word1的前i-1个字符匹配到word2的前j个字符,操作次数为dp[i - 1][j] + 1。 - 插入
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 表格会填充如下:
| '' | r | o | s | |
|---|---|---|---|---|
| '' | 0 | 1 | 2 | 3 |
| h | 1 | 1 | 2 | 3 |
| o | 2 | 2 | 1 | 2 |
| r | 3 | 2 | 2 | 2 |
| s | 4 | 3 | 3 | 2 |
| e | 5 | 4 | 4 | 3 |
最终结果 dp[5][3] = 3,表示将 horse 变成 ros 需要 3 次操作。
编辑距离总结篇
做一个总结吧