目录
LeetCode115.不同的子序列
LeetCode583.两个字符串的删除操作
LeetCode72.编辑距离
给你两个字符串
s
和t
,统计并返回在s
的 子序列 中t
出现的个数,结果需要对 10^9 + 7 取模。
思路:昨天做了一道判断子序列的问题,今天这个跟它有点区别,这里是问子序列的个数有多少个。
但是大体上其实就是分为两个部分,遇到元素相等时如何处理,不相等时又如何处理。
这里当元素相等的时候,有两种情况。
一种是以s[i-1]元素去匹配,一种是不以s[i-1]去匹配。
这里建议参考s="bagg",t="bag"这个例子来看。
当是第一种情况的时候,这个时候就等于dp[i-1][j-1],相当于使用s[i-1]来匹配了,所以不用管s[i-1],t[j-1]的情况了(因为这是在两者相等的情况下进行的比较),所以这种情况下的个数取决于前面的状态数;
第二种情况下,相当于不拿这个相等的元素s[i-1]去匹配,而是看不适用这个元素能否有可能将以j-1下标结尾的t字符串进行匹配,所以这里是dp[i-1][j],因为dp[i-1][j]早已经将前面的状态记录下来了,有没有,有多少都在这里面记录着。
最后将上面两种情况相加,即得到了在元素相等的情况下的dp[i][j]的数量。
而对于当s[i-1]不等于t[j-1]时,那就不可能有s[i-1]去匹配的情况发生,所以就只剩下dp[i-1][j],也就是之前的记录状态,所以这时dp[i][j]=dp[i-1][j]。
int numDistinct(string s, string t) {
//dp[i][j]表示以下标i-1结尾的s字符串包含以下标j-1结尾的t字符串的个数
vector> dp(s.size() + 1, vector(t.size() + 1, 0));//这里的uint64_t类型是处理大数据范围的
for(int i = 0; i < s.size() + 1; i ++) dp[i][0] = 1;//这里表示以下标i-1结尾的s字符串包含空字符串的个数,这里可以很容易想到应该赋值为1;
for(int j = 1; j < t.size() + 1; j ++) dp[0][j] = 0;//这里表示空字符串的s字符串包含空以下标j-1结尾的t字符串的个数,应该初始化为0。这里需要注意dp[0][0]表示空字符串的s包含空字符串的t的个数,明显dp[0][0]值为1,所以这里的起始下标从1开始。
for(int i = 1; i < s.size() + 1; i ++){
for(int j = 1; j < t.size() + 1; j ++){
if(s[i - 1] == t[j - 1]){
//当元素相等时,有两种方式获取个数
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
//第一种是使用这个相等的元素s[i-1]匹配,dp[i-1][j-1]表示dp[i][j]表示以下标i-2结尾的s字符串包含以下标j-2结尾的t字符串的个数,
//第二种是不使用这个相等元素s[i-1]匹配,而是尝试使用dp[i-1][j],即以下标i-2结尾的s字符串包含以下标j-1结尾的t字符串的个数,
//这里其实相当于是记录下了前面的状态个数有多少个,然后累加上这里使用相等元素s[i-1]匹配的时候的个数,总数即为最终结果
}else{
dp[i][j] = dp[i - 1][j];//这里s[i-1]与t[j-1]没有相等,所以相当于只能使用前面已经记录好的状态个数
}
}
}
return dp[s.size()][t.size()];
}
时间复杂度:O(n*m)
时间复杂度:O(n*m)
给定两个单词
word1
和word2
,返回使得word1
和word2
相同所需的最小步数。每步 可以删除任意一个字符串中的一个字符。
思路:刚看到这道题的时候其实就应该有一个思路,那就是求最大公共子序列,然后用两个字符串的长度分别减去最大公共子序列的长度,即可得到两者分别需要删除的最小步数,两者需要删除的最小步数相加即得到了最终结果。
其实还是比较清晰的,最大公共子序列之前也讲过,如果熟悉的话这道题还是比较简单的。
int minDistance(string word1, string word2) {
//dp[i][j]表示以下标i-1结尾的word1字符串与以下标j-1结尾的word2字符串的最长公共子序列长度
vector> dp(word1.size() + 1, vector(word2.size() + 1, 0));
for(int i = 1; i < word1.size() + 1; i ++){
for(int j = 1; j < word2.size() + 1; j ++){
if(word1[i - 1] == word2[j - 1]){
dp[i][j] = dp[i - 1][j - 1] + 1;
}else{
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return word1.size() + word2.size() - dp[word1.size()][word2.size()] * 2;
}
时间复杂度:O(n*m)
空间复杂度:O(n*m)
当然还有一种思路。关键还是在处理元素相等时与不相等时。
当元素相等的时候,保持之前的状态即可,即dp[i][j]=dp[i-1][j-1];
当元素不相等的时候,这里就有三种情况可选。
第一种是word1删除一个元素,回退一个下标,操作步数为1,即dp[i-1][j] + 1;
第二种是word2删除一个元素,同样回退一个下标,操作步数为1,即dp[i][j-1]+1;
第三种是word1、word2分别删除一个元素,回退一个下标,这时操作步数为2,所以有dp[i-1][j-1]+2。
但是事实上,第一、二种与第三种的状态存在重叠部分,拿第一种与第三种来说,dp[i-1][j-1]就相当于dp[i][j]中的word1、word2各删除了一个元素,操作步数为2,而dp[i-1][j]+1相当于dp[i][j]在本身word1回退删除了一个元素的基础上,尝试使得word2删除一个元素,操作步数为1。
同样的对于第二、第三种情况也是同样的道理,他们存在一个逻辑上的重叠。
因此可以将dp[i-1][j-1]+2去除,保留第一、二种情况,取其中的最小值。
当然如果没有理解这个去重逻辑也没关系,在元素不相等时从三种情况中取最小值也可以,也能通过。
int minDistance(string word1, string word2) {
//dp[i][j]表示以下标i-1结尾的word1以及以下标j-1结尾的word2相同时所需的最小步数
vector> dp(word1.size() + 1, vector(word2.size() + 1));
for(int i = 0; i < word1.size() + 1; i ++) dp[i][0] = i;//这里表示以下标i-1结尾的word1与空字符串的word2相同时所需的最小步数,当然是将word1中的元素全部删除才能相等,所以dp[i][0]=i
for(int j = 0; j < word2.size() + 1; j ++) dp[0][j] = j;//同理,对于dp[0][j]也是同样的道理
for(int i = 1; i < word1.size() + 1; i ++){
for(int j = 1; j < word2.size() + 1; j ++){
if(word1[i - 1] == word2[j - 1]){
dp[i][j] = dp[i - 1][j - 1];//这里在word1[i-1]与word2[j-1]相等后,说明不需要进行任何操作,所以它的最小步数就是前面dp[i-1][j-1]的步数
}else{
dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + 1;
//这里其实应该有三种情况可以选
//第一种是dp[i-1][j] + 1,表示以下标i-2结尾的word1以及以下标j-1结尾的word2相同时所需的最小步数的基础上,删除掉当前这个不相等的元素,这里只对word1删除了元素,所以需要加1
//第一种是dp[i][j-1] + 1,表示以下标i-1结尾的word1以及以下标j-2结尾的word2相同时所需的最小步数的基础上,删除掉当前这个不相等的元素,这里只对word2删除了元素,所以需要加1
//第三种情况第一种是dp[i-1][j-1] + 2,表示以下标i-2结尾的word1以及以下标j-2结尾的word2相同时所需的最小步数的基础上,删除掉当前这个不相等的元素,这里是对word1和word2都进行了操作,所以需要加2
//当时其实dp[i-1][j]或者dp[i][j-1]和dp[i-1][j-1]具有关联
//这里以dp[i-1][j]来说明,dp[i-1][j]+1和dp[i-1][j-1]+2其实在逻辑上是可以说它两是相等的,为什么呢?
//dp[i-1][j]+1可以看作是dp[i][j]开始,在word1中删除一个元素,进行了一步操作所得,同样的dp[i-1][j-1]+2可以看作是dp[i][j]开始,在word1、word2中各删除一个元素的基础上,操作两步所得。所以如果只操作一次,那会不会就是dp[i-1][j]+1呢,显然答案是会,因此在逻辑上存在重叠
//同理对于dp[i][j-1]也可以这样理解,所以这里就将其简化为dp[i-1][j]+1和dp[i][j-1]+1的比较了,取最小的值
}
}
}
return dp[word1.size()][word2.size()];
}
时间复杂度:O(n*m)
空间复杂度:O(n*m)
给你两个单词
word1
和word2
, 请返回将word1
转换成word2
所使用的最少操作数 。你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
思路:这里和前面的题的区别在于这里新增了几个操作,不仅有删除,还有插入与替换。
还是从元素相等与不相等来分析。
当word1[i-1]等于word2[j-1]时,这时候不需要操作,于是保持前面的最少操作数即可,即为dp[i-1][j-1];
当word1[i-1]不等于word2[j-1]时,这时候分三种情况
第一种是删除元素,word1中删除元素,操作数为1,所以等于dp[i-1][j]+1;
第二种是插入元素,也相当于添加元素,采用的是word2删除元素,即dp[i][j-1]+1;
为什么呢?
因为它们是等价的操作数量,例如word1为“a",word2为“ab”,让word1插入一个“b”的操作次数使这两个字符串相等其实和word2删除一个“b”的操作次数使这两个字符串相等是一样的,所以这里使用word2删除元素来替代了word1插入元素的次数,为dp[i][j-1]+1;
第三种是替换,之前说元素相等的时候dp[i][j]=dp[i-1][j-1],当元素不相等的时候,不需要删除或者插入元素,而是将不相等的元素换成相等的元素,这样以i-1结尾的字符串word1就能和以j-1结尾的字符串word2相等了,这时操作次数为1,所以这里dp[i][j]=dp[i-1][j-1]+1。
在元素不相等的时候,以上三种情况取最小的即可。
int minDistance(string word1, string word2) {
//dp[i][j]表示以下标i-1结束的word1转换成以下标j-1结束的word2所需使用的最少操作数量
vector> dp(word1.size() + 1, vector(word2.size() + 1));
for(int i = 0; i < word1.size() + 1; i ++) dp[i][0] = i;//这里相当于word1需要删除i个元素才能和空字符串word2相等,也就是转换成功
for(int j = 0; j < word2.size() + 1; j ++) dp[0][j] = j;//这里相当于word1需要插入j个元素才能和word2字符串相等
for(int i = 1; i < word1.size() + 1; i ++){
for(int j = 1; j < word2.size() + 1; 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], min(dp[i][j - 1], dp[i - 1][j - 1])) + 1;
//元素不相等的时候,有三种操作情况
//第一种是word1删除元素,回退一个下标,操作次数为1,所以为dp[i-1][j]+1;
//第二种是word1插入元素,但是实际上word1的插入元素的操作数量等价于word2的删除元素的操作数量,例如word1为“a",word2为“ab”,让word1插入一个“b”的操作次数其实和word2删除一个“b”的操作次数是一样的,所以这里使用word2删除元素来替代了word1插入元素的次数,为dp[i][j-1]+1
//第三种是替换字符,我们知道当相等的时候dp[i][j]=dp[i-1][j-1],操作次数不会增加,所以如果尝试在word1中替换不相等的元素为相等的元素,那么操作次数就应该会加1,所以dp[i][j]=dp[i-1][j-1]+1
//以上三种情况在word1中的字符与word2中的字符不相等时取最小的
}
}
}
return dp[word1.size()][word2.size()];
}
时间复杂度:O(n*m)
空间复杂度:O(n*m)
感谢你的阅读,希望我的文章能够给你帮助,如果有帮助,麻烦点赞加收藏,或者点点关注,非常感谢。
如果有什么问题欢迎评论区讨论!