前言
终于,荔枝动态规划总结到编辑距离类型的题目了!在这篇文章中荔枝接着按照这个动态规划系列的问题总结的思路继续梳理,首先主要是通过两个只有删除操作的例题来体会做编辑距离问题的思路,并在之后加入有增删改操作的例题,系列文章即将结束,小伙伴们有需要的话可以看我的专栏里面的系列文章,希望能帮助到有需要的小伙伴~~~
文章目录
一、Leecode392.判断子序列
题目描述:
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(如,"ace"是"abcde"的一个子序列,而"aec"不是)。
输入样例:
s = "abc", t = "ahbgdc"
输出样例:
true
1.1 思路分析
通过前面四个系列问题的学习以及总结其实大家一定和荔枝一样都对动态规划的解题步骤都熟记于心了,下面我们也要用这些方法来辅助我们解题。首先分析一下题干,题目要求通过删除操作的判断方法来判断一个字符串是不是另一个字符串的子序列,大家是不是想到了这道题目其实和求解最大公共子序列有关系呢?是的,我们可以转化为求解最大公共子序列,最后再来判断s是不是t的子序列。
明确dp数组的含义
dp[i][j]:下标为i-1结尾的字符串s和以下标为j-1结尾的字符串t的相同子序列的长度是dp[i][j]
注意:
之所以要这么定义数组含义是为了能够减少初始化的步骤,也就相当于在一个二维的数组网格的最外围四个方向再多加一层有初始化值为0的dp数组元素,从而在动态规划的过程中程序自行初始化值。
dp数组推导式
在确定dp数组的推导式的时候我们需要明确两种情况:s[i-1]==t[j-1]时,字符串的长度就取决于s[i-2]和t[j-2]时候的dp[i-1][j-1]+1;当s[i-1]!=t[j-1]时,因为我们需要比较的是s是否是t的子串,所以此时只有t在做删除的操作,也就是要将t[i-1]删除,那么此时dp[i][j]的值就取决于s[i-1]和t[j-2]的比较结果了。
if(s[i-1]==t[j-1]){
dp[i][j] = dp[i-1][j-1]+1;
}else{
dp[i][j] = dp[i][j-1];
}
确定初始化条件以及遍历顺序
对于初始化我们根据上述步骤推出来的dp递推式可以知晓,当前状态两个字符串的判断是受到前一个状态的影响的。对于题目分析后我们需要确定dp[1][j]和dp[i][1],而这是由dp[0][j]和dp[i][0]推导出来的,dp[0][j]和dp[i][0]各自代表着对应字符串和空串之间的公共子序列,因此我们将其全部初始化为0。遍历顺序依旧根据递推公式按照从左到右、从上到下来遍历。
1.2 题解示例
class Solution {
public:
bool isSubsequence(string s, string t) {
vector<vector<int>> dp(s.size() + 1, vector<int>(t.size() + 1, 0));
for (int i = 1; i <= s.size(); i++) {
for (int j = 1; j <= t.size(); j++) {
if (s[i - 1] == t[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
else dp[i][j] = dp[i][j - 1];
}
}
if (dp[s.size()][t.size()] == s.size()) return true;
return false;
}
};
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/is-subsequence
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
二、Leecode115.不同的子序列
题目描述:
给你两个字符串 s 和 t ,统计并返回在 s 的 子序列 中 t 出现的个数。题目数据保证答案符合 32 位带符号整数范围。
输入样例:
s = "rabbbit", t = "rabbit"
输出样例:3
输出解释:
如下所示, 有 3 种可以从 s 中得到 "rabbit" 的方案。
rabbbit
rabbbit
rabbbit
2.1 思路分析
通过上一题和前面的子序列和子串系列问题,我们可以清楚编辑距离类型的问题对于dp数组的定义是大体上一致的。
明确dp数组的含义
dp[i][j]:下标为i-1结尾的字符串s出现以下标为j-1结尾的字符串t的个数
dp数组的推导式
这道题目求的不是字符串的长度而是字符串的个数,根据题意中给出的解释我们知道,在遍历的末尾元素是相同的情况下,dp[i][j]可以由两个状态推导出,即当前遍历的是末两位元素是一样的情况下,dp[i][j] = dp[i-1][j-1] + dp[i-1][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];
}
确定初始化条件和遍历顺序
根据递推公式我们必须要事先初始化好dp[i][0]和dp[0][j],当t为空字符串的时候s仅需要删除所有的元素即可达到;当s为空字符串的时候t只要是非空子串就一定不符合题目要求。
vector<vector<uint64_t>> dp(s.size() + 1, vector<uint64_t>(t.size() + 1));
for (int i = 0; i < s.size(); i++) dp[i][0] = 1;
for (int j = 1; j < t.size(); j++) dp[0][j] = 0;
2.2 题解示例
class Solution {
public:
int numDistinct(string s, string t) {
vector<vector<uint64_t>> dp(s.size() + 1, vector<uint64_t>(t.size() + 1));
for (int i = 0; i < s.size(); i++) dp[i][0] = 1;
for (int j = 1; j < t.size(); j++) dp[0][j] = 0;
for (int i = 1; i <= s.size(); i++) {
for (int j = 1; j <= t.size(); 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.size()][t.size()];
}
};
// dp[i][j]:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]
// dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]
// dp[i][j] = dp[i - 1][j];
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/distinct-subsequences
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
三、 Leecode583.两个字符串的删除操作
题目描述:
给定两个单词 word1 和 word2 ,返回使得 word1 和 word2 相同所需的最小步数。
每步 可以删除任意一个字符串中的一个字符。
输入样例:
输入: word1 = "sea", word2 = "eat"
输出样例:2
解释:第一步将 "sea" 变为 "ea" ,第二步将 "eat "变为 "ea"
3.1 思路分析
明确dp数组的含义
dp[i][j]:以i-1为结尾的字符串word1和以j-1位结尾的字符串word2想要达到相等,所需要删除元素的最少次数。
dp数组的推导式
这道题目求的是最少执行删除操作的步数,这里我们需要弄清楚dp数组的含义。对于i-1位的word1和j-1位word2如果是相同的,其需要删除的步数和前一个状态相同;如果不同的话,那么就需要分别讨论两个字符串各自前一个状态的最小操作步数+一个删除操作。
if (word1[i - 1] == word2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1);
}
确定初始化条件和遍历顺序
dp[i][0]:word2为空字符串,以i-1为结尾的字符串word1要删除多少个元素,才能和word2相同呢,很明显dp[i][0] = i,同理,dp[0][j] = j。
for (int i = 0; i <= word1.size(); i++) dp[i][0] = i;
for (int j = 0; j <= word2.size(); j++) dp[0][j] = j;
3.2 题解示例
class Solution {
public:
int minDistance(string word1, string word2) {
vector<vector<int>> dp(word1.size() + 1, vector<int>(word2.size() + 1));
for (int i = 0; i <= word1.size(); i++) dp[i][0] = i;
for (int j = 0; j <= word2.size(); j++) dp[0][j] = j;
for (int i = 1; i <= word1.size(); i++) {
for (int j = 1; j <= word2.size(); j++) {
if (word1[i - 1] == word2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1);
}
}
}
return dp[word1.size()][word2.size()];
}
};
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/delete-operation-for-two-strings
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
四、Leecode72.编辑距离
题目描述:
给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符
输入样例:word1 = "horse", word2 = "ros"
输出样例:3
输出解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
4.1 思路分析
明确dp数组的含义
dp[i][j]:以下标i-1为结尾的字符串word1和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]。
dp数组的推导式
这道题目求的是最少执行编辑操作的步数,和上一题两个字符串的删除操作类似,只是这里由增加和替换的操作。当word1和word2当前遍历的元素不一致的时候,就需要取三种操作中编辑距离最小的一种。需要注意的是:word1执行删除操作等于word2执行增加操作。因此对比于上题我们只需要考虑加入替换操作的最小编辑距离即可
if(word1[i-1]==word2[j-1]){
dp[i][j] = dp[i-1][j-1];
}else{
dp[i][j] = min({dp[i-1][j-1]+1,dp[i-1][j]+1,dp[i][j-1]+1});
}
确定初始化条件和遍历顺序
这里的初始化和遍历同上一题两个字符串的删除操作思路一样,这里就不再过多赘述了。
4.2 题解示例
class Solution {
public:
int minDistance(string word1, string word2) {
vector<vector<int>> dp(word1.size()+1,vector<int>(word2.size()+1,0));
for(int i=0;i<=word1.size();i++) dp[i][0] = i;
for(int j=0;j<=word2.size();j++) dp[0][j] = j;
for(int i=1;i<=word1.size();i++){
for(int j=1;j<=word2.size();j++){
if(word1[i-1]==word2[j-1]){
dp[i][j] = dp[i-1][j-1];
}else{
dp[i][j] = min({dp[i-1][j-1]+1,dp[i-1][j]+1,dp[i][j-1]+1});
}
}
}
return dp[word1.size()][word2.size()];
}
};
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/edit-distance
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
总结
荔枝终于整理完成了动态规划的经典系列问题了哈哈哈,收获还是有一些的。其实一遍走下来发现动态规划最难的不是步骤的复杂,而是dp数组的定义以及推导式的思考。什么场景下需要加上前面的结果,什么场景下不用,什么时候有需要将dp数组特殊处理一下?这些是值得我们仔细想想的地方。一开始直接做编辑距离确实有点抽象,但是通过前面的三道难度逐步增加的例题铺垫,最后再刷编辑距离其实感觉倒是没有那么难了哈哈哈。最后荔枝希望自己能够继续坚持下去,也希望能够跟大家一起共勉!
今朝已然成为过去,明日依然向往未来!我是小荔枝,在技术成长的路上与你相伴,码文不易,麻烦举起小爪爪点个赞吧哈哈哈~~~ 比心心♥~~~