代码随想录 | Day 55 - LeetCode 392. 判断子序列、LeetCode 115. 不同的子序列

        今天依然是子序列问题的延续,要比之前的内容难。这两道题都涉及到主串和模式串情境下“删除主串末尾元素”的思想。第1题重在问题的转化,将“是或不是子串的判断问题”转化为了“计算两个数组的最大相同子序列长度”问题;第2题则要仔细思考出现次数的来源,以及初始化方面的物理含义。


        第1题(LeetCode 392. 判断子序列)需要首先将问题转化为“求s和t的最大相同子序列长度”,用dp[i][j]表示“s[0 : i - 1]和t[0 : j - 1]的最大相同子序列长度”(使用i - 1、j - 1而不用i、j的原因与之前一样,还是为了后面的初始化方便)。这里要注意,s是较短的字符串, t是较长的字符串

        与之前的题目一样,当s[i - 1]与t[j - 1]相等时,dp[i][j]需要在dp[i - 1][j - 1]的基础上加1;而两者不相等时,因为s较短,t较长,题目要求s包含于t内,所以要删除t[j - 1],取dp[i][j - 1]。这里不应该考虑删除s[i - 1],取dp[i - 1][j],因为s包含于t内,删除s的末尾得到的dp数值是没有意义的。

        这样一来,dp每个位置的数值都依赖于其左上方和左方,所以需要初始化第0行和第0列。其分别对应s为空和t为空的情况,所以都初始化为0即可。也正因为这个依赖关系,所以内外层循环的遍历方向都为正向。最终如果dp右下角的值与s的长度相等,则代表s是t的子序列;否则就不是。

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]) { // 不是s[i] == t[j]
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                }
                else {
                    dp[i][j] = dp[i][j - 1];
                }
            }
        }
        return dp[s.size()][t.size()] == s.size();
    }
};

        这道题还有时间复杂度更优的双指针解法,两个指针分别位于s和t中。不断的右移t指针,使t指针所指的值匹配s指针所指的值。匹配成功后s也右移,否则就只有t右移。如果s指针最终能到达s的末尾,说明s是t的子序列;否则就不是。

class Solution {
public:
    bool isSubsequence(string s, string t) {
        int indS = 0, indT = 0;
        while (indS < s.size() && indT < t.size()) {
            if (s[indS] == t[indT]) {
                ++indS, ++indT;
            }
            else {
                ++indT;
            }
        }
        return indS == s.size();
    }
};

        第2题(LeetCode 115. 不同的子序列)相比上一题就难很多,自己没有做出来。题解将dp[i][j]定义为s[0 :  - 1]中出现t[0 : j - 1]的次数(使用i - 1、j - 1而不用i、j的原因与之前一样,还是为了后面的初始化方便)。注意这道题中s较长,t较短,与上一题刚好相反。

        这道题同样分s[i - 1]与t[j - 1]相等和不相等两种情况。相等时,出现次数一部分来源于s[0 : i - 2]中t[0 : j - 2]的出现次数,对应dp[i - 1][j - 1]。另一部分则来源于s[0 : i - 2]中t[0 : j - 1]的出现次数(即删除掉s[i - 1]),对应dp[i - 1][j]。前者比较容易理解,后者是类似“s[0 : i - 1]为dogg,t[0 : j - 1]为dog”的情况,不仅应该计算dogdo的出现次数,还应该加上dogdog的出现次数。而不相等时,出现次数则只能来源于s[0 : i - 2]中t[0 : j - 1]的出现次数(即删除掉s[i - 1]),对应dp[i - 1][j]

        所以当前位置的dp值依赖于其左上方和上方位置,就需要初始化第0行和第0列。其分别对应s为空和t为空的情况。第0列,对应t为空,dp[i][0]对应s[0 : i - 1]“删除任意字符后出现空字符”的个数,只有删除全部元素时才能出现空,所以只有一种删除方式,dp[i][0]都应该初始化为1;第0行,对应s为空,dp[0][j](j > 0)对应空字符“删除任意字符后出现t[0 : i - 1]”的个数,空字符无论怎么删除都还是空字符,所以有0中删除方式,dp[0][j](j > 0)都应该初始化为0。而特殊的dp[0][0],则代表空字符“删除任意字符后出现空字符”的个数,也只有“不做任何删除”这一种方法,所以应该初始化为1。

        也正因为上一段开头提到的依赖关系,所以双层循环的遍历方向都应该是正向。最终返回dp右下角的值作为结果。

class Solution {
public:
    int numDistinct(string s, string t) {
        vector<vector<unsigned long long>> dp(s.size() + 1, vector<unsigned long long>(t.size() + 1, 0));
        for (int i = 0; i <= s.size(); ++i) {
            dp[i][0] = 1;
        }
        for (int i = 1; i <= s.size(); ++i) {
            for (int j = 1; j <= t.size(); ++j) {
                if (s[i - 1] == t[j - 1]) { // 不是s[i] == t[j]
                    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()];
    }
};

        这道题的测试用例中会包含超出long long范围的数字,所以要将dp类型设置为unsigned long long,也就是uint64_t(第一次遇到,具体可见这里)。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值