代码随想录算法训练营第45天 | LeetCode115.不同的子序列、 LeetCode583.两个字符串的删除操作、LeetCode72.编辑距离

目录

LeetCode115.不同的子序列

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

LeetCode72.编辑距离


LeetCode115.不同的子序列

给你两个字符串 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)

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

给定两个单词 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)

LeetCode72.编辑距离

给你两个单词 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)

感谢你的阅读,希望我的文章能够给你帮助,如果有帮助,麻烦点赞加收藏,或者点点关注,非常感谢。

如果有什么问题欢迎评论区讨论!

 

你可能感兴趣的:(算法,数据结构,动态规划)