• 代码随想录 | 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的子序列;否则就不是。

    1. class Solution {
    2. public:
    3. bool isSubsequence(string s, string t) {
    4. vectorint>> dp(s.size() + 1, vector<int>(t.size() + 1, 0));
    5. for (int i = 1; i <= s.size(); ++i) {
    6. for (int j = 1; j <= t.size(); ++j) {
    7. if (s[i - 1] == t[j - 1]) { // 不是s[i] == t[j]
    8. dp[i][j] = dp[i - 1][j - 1] + 1;
    9. }
    10. else {
    11. dp[i][j] = dp[i][j - 1];
    12. }
    13. }
    14. }
    15. return dp[s.size()][t.size()] == s.size();
    16. }
    17. };

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

    1. class Solution {
    2. public:
    3. bool isSubsequence(string s, string t) {
    4. int indS = 0, indT = 0;
    5. while (indS < s.size() && indT < t.size()) {
    6. if (s[indS] == t[indT]) {
    7. ++indS, ++indT;
    8. }
    9. else {
    10. ++indT;
    11. }
    12. }
    13. return indS == s.size();
    14. }
    15. };

            第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右下角的值作为结果。

    1. class Solution {
    2. public:
    3. int numDistinct(string s, string t) {
    4. vectorunsigned long long>> dp(s.size() + 1, vector<unsigned long long>(t.size() + 1, 0));
    5. for (int i = 0; i <= s.size(); ++i) {
    6. dp[i][0] = 1;
    7. }
    8. for (int i = 1; i <= s.size(); ++i) {
    9. for (int j = 1; j <= t.size(); ++j) {
    10. if (s[i - 1] == t[j - 1]) { // 不是s[i] == t[j]
    11. dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
    12. }
    13. else {
    14. dp[i][j] = dp[i - 1][j];
    15. }
    16. }
    17. }
    18. return dp[s.size()][t.size()];
    19. }
    20. };

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

  • 相关阅读:
    【Oracle APEX开发小技巧2】在不通过类型转换的前提下使用Oracle APEX自带的格式掩码实现数值的精确展现
    Appium 使用
    【echart 】legend.icon传递svg图标,图标不显示的原因。
    Win11打不开组策略编辑器怎么办
    MongoDB ObjectId() 是如何实现的 “千万级” 分布式唯一 ID?
    Go语法实现分析之chan、go func、类型转换
    计算机毕设(附源码)JAVA-SSM旅游网站设计
    腾讯云新客户优惠服务器88元/年,540元/3年,另有5年优惠服务器
    Linux常用命令——常用网络命令【二】
    elasticsearch 映射
  • 原文地址:https://blog.csdn.net/weixin_43469527/article/details/127877012