Day45-动态规划,leetcode115,583,72
- 不同的子序列
- 给你两个字符串 s 和 t ,统计并返回在 s 的 子序列 中 t 出现的个数。
- 测试用例保证结果在 32 位有符号整数范围内。
- 思路
- 字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串
- 1.确定dp数组定义及下标的含义:dp[i][j]:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]
- 2.确定递推公式:分两种情况,(1)s[i - 1] 与 t[j - 1]相等,可以选择用s[i - 1]来匹配,也可以选择不用s[i - 1]来匹配,即 dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];(2)s[i - 1] 与 t[j - 1] 不相等,只能考虑不用s[i - 1]匹配,dp[i][j] = dp[i - 1][j];
- 3.dp数组如何初始化:dp[i][0] ,以i-1为结尾的s可以随便删除元素,出现空字符串的个数。那么dp[i][0]=1;dp[0][j]:空字符串s可以随便删除元素,出现以j-1为结尾的字符串t的个数。那么dp[0][j]=0,s如论如何也变成不了t。dp[0][0]=1,空字符串s,可以删除0个元素,变成空字符串t。
- 4.确定遍历顺序:从递推公式dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]; 和 dp[i][j] = dp[i - 1][j]; 中可以看出dp[i][j]都是根据左上方和正上方推出来的。所以遍历的时候一定是从上到下,从左到右,这样保证dp[i][j]可以根据之前计算出来的数值进行计算。
- 5.举例推导dp数组,打印dp数组:
/**
* 1. dp数组定义
dp[i][j] 表示:s 的前 i 个字符中,t 的前 j 个字符作为子序列出现的个数。
* 2. 初始化
空字符串 t 在任何 s 的前缀中都只出现一次(即都可以通过删除所有字符得到空串),所以 dp[i][0] = 1。
其他 dp[0][j] 默认是 0,因为空字符串 s 无法变成非空字符串 t。
* 3. 状态转移
如果 s[i-1] === t[j-1],可以选择用 s[i-1] 匹配 t[j-1](即 dp[i-1][j-1]),也可以不用 s[i-1](即 dp[i-1][j]),两种情况之和。
如果 s[i-1] !== t[j-1],只能不用 s[i-1],即 dp[i-1][j]。
* 4. 返回结果
dp[s.length][t.length] 就是 s 中 t 作为子序列出现的总个数。
* 用二维动态规划,统计 s 的每个前缀中 t 的每个前缀作为子序列出现的次数,最终返回总数。
*/
const numDistinct = (s, t) => {
let dp = Array.from(Array(s.length + 1), () => Array(t.length +1).fill(0));
for(let i = 0; i <=s.length; i++) {
dp[i][0] = 1;
}
for(let i = 1; i <= s.length; i++) {
for(let j = 1; j<= t.length; j++) {
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]
}
}
}
return dp[s.length][t.length];
};
- 两个字符串的删除操作
- 给定两个单词 word1 和 word2 ,返回使得 word1 和 word2 相同所需的最小步数。
- 每步 可以删除任意一个字符串中的一个字符。
- 思路
- 1.确定dp数组定义及下标的含义:dp[i][j]:以i-1为结尾的字符串word1,和以j-1为结尾的字符串word2,想要达到相等,所需要删除元素的最少次数。
- 2.确定递推公式:
- 当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] + 1
- 情况二:删word2[j - 1],最少操作次数为dp[i][j - 1] + 1
- 情况三:同时删word1[i - 1]和word2[j - 1],操作的最少次数为dp[i - 1][j - 1] + 2
- 三种情况取最小值,递推公式:dp[i][j] = min({dp[i - 1][j - 1] + 2, dp[i - 1][j] + 1, dp[i][j - 1] + 1})
- 3.dp数组如何初始化:dp[i][0]:word2为空字符串,以i-1为结尾的字符串word1要删除多少个元素,才能和word2相同呢,dp[i][0] = i。同理dp[0][j] = j。
- 4.确定遍历顺序:从上到下,从左到右
- 5.举例推导dp数组,打印dp数组:
/**
* 1. dp数组定义
dp[i][j] 表示:将 word1 的前 i 个字符和 word2 的前 j 个字符变成相同所需的最少删除次数。
* 2. 初始化
如果 word2 是空串,word1 需要删掉所有字符才能相同,所以 dp[i][0] = i。
如果 word1 是空串,word2 需要删掉所有字符才能相同,所以 dp[0][j] = j。
* 3. 状态转移
如果当前字符相同,不需要删除,直接继承左上角的值。
如果不同,有三种选择,取最小值:
删除 word1[i-1],即 dp[i-1][j] + 1
删除 word2[j-1],即 dp[i][j-1] + 1
同时删除两者,dp[i-1][j-1] + 2
* 4. 返回结果
dp[word1.length][word2.length] 就是将两个字符串变成相同所需的最少删除次数。
* 用动态规划,逐步计算每个前缀变成相同的最少删除次数,最终返回全局最优解。
*/
var minDistance = (word1, word2) => {
let dp = Array.from(new Array(word1.length + 1), () =>
Array(word2.length + 1).fill(0)
);
for (let i = 1; i <= word1.length; i++) {
dp[i][0] = i;
}
for (let j = 1; j <= word2.length; j++) {
dp[0][j] = j;
}
for (let i = 1; i <= word1.length; i++) {
for (let j = 1; j <= word2.length; 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[word1.length][word2.length];
};
- 编辑距离
- 给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。
- 你可以对一个单词进行如下三种操作:插入一个字符、删除一个字符、替换一个字符
- 思路
- 1.确定dp数组定义及下标的含义:dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]。
- 2.确定递推公式:
- if (word1[i - 1] == word2[j - 1]) 那么说明不用任何编辑,dp[i][j] 就应该是 dp[i - 1][j - 1],即dp[i][j] = dp[i - 1][j - 1];
- if (word1[i - 1] != word2[j - 1]),此时就需要编辑了
- 操作一:word1删除一个元素,那么就是以下标i - 2为结尾的word1 与 j-1为结尾的word2的最近编辑距离 再加上一个操作。即 dp[i][j] = dp[i - 1][j] + 1;
- 操作二:word2删除一个元素,那么就是以下标i - 1为结尾的word1 与 j-2为结尾的word2的最近编辑距离 再加上一个操作。即 dp[i][j] = dp[i][j - 1] + 1;
- 操作三:替换元素,word1替换word1[i - 1],使其与word2[j - 1]相同,此时不用增删加元素。即 dp[i][j] = dp[i - 1][j - 1] + 1;
- 以上三种情况取最小值,即:dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1;
- 3.dp数组如何初始化:dp[i][0] :以下标i-1为结尾的字符串word1,和空字符串word2,最近编辑距离为dp[i][0],对word1里的元素全部做删除操作,即:dp[i][0] = i;同理dp[0][j] = j;
- 4.确定遍历顺序:从左到右,从上到下
- 5.举例推导dp数组,打印dp数组:
/**
* 1. dp数组定义
dp[i][j] 表示:将 word1 的前 i 个字符和 word2 的前 j 个字符变成相同所需的最少操作数。
* 2. 初始化
dp[i][0]:把 word1 的前 i 个字符变成空串,需要删除 i 次。
dp[0][j]:把空串变成 word2 的前 j 个字符,需要插入 j 次。
* 3. 状态转移
如果当前字符相同,不需要操作,直接继承左上角的值。
如果不同,有三种操作,取最小值:
删除 word1[i-1]:dp[i-1][j] + 1
插入 word2[j-1]:dp[i][j-1] + 1
替换 word1[i-1]:dp[i-1][j-1] + 1
* 4. 返回结果
dp[word1.length][word2.length] 就是将 word1 转换成 word2 的最少操作数。
用动态规划,逐步计算每个前缀之间的最少编辑距离,最终返回全局最优解。
*/
const minDistance = (word1, word2) => {
let dp = Array.from(Array(word1.length + 1), () => Array(word2.length+1).fill(0));
for(let i = 1; i <= word1.length; i++) {
dp[i][0] = i;
}
for(let j = 1; j <= word2.length; j++) {
dp[0][j] = j;
}
for(let i = 1; i <= word1.length; i++) {
for(let j = 1; j <= word2.length; 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] + 1, dp[i-1][j-1] + 1);
}
}
}
return dp[word1.length][word2.length];
};
参考&感谢各路大神
宝剑锋从磨砺出,梅花香自苦寒来。