115. 不同的子序列

115. 不同的子序列

LeetCode题目:115. 不同的子序列

我的思路

  • 字符串 + 出现个数,显然这极大可能是一个 动态规划 的题目。

  • 1.确定数组f的含义和结构。显然,s的长度和t的长度均会对最终的结果造成影响。

    • f[i][j]的含义是:字符串s[0:i]和字符串t[0:j],s[0:i]的子序列中t[0:j]出现的个数。
    • 那么,最终的结果就是f[s.size()-1][t.size()-1].
  • 2.确定转移状态。我们应该如何求解f[i][j]?

    • 假设 s=“babgbag”,t=“bag”。

    • 假设我们现在的状态是:

      s => babgbag
              ↑
              i
      t => bag
            ↑
            j
      
      • s[i] != t[j],那么,f[i][j] = f[i-1][j]
    • 假设我们现在的状态是:

      s => babgbag
                ↑
                i
      t => bag
            ↑
            j
      
      • s[i] = t[j],那么,f[i][j] = f[i-1][j-1] + f[i-1][j]
    • 那么我们得到了转移方程:

      f[i][j] = {
          f[i-1][j] , s[i] = t[j];
          f[i-1][j-1] + f[i-1][j], s[i] != t[j];
      }
      
    • 其实上述转移方程是不完全的。因为,在写上述转移方程的时候,我们有一个隐含的条件:i >= j. 因为根据题目含义,当 i < j时,f[i][j] = 0。

    • 真正的转移方程:

      f[i][j] = {
          f[i-1][j] , s[i] = t[j] && i >= j;
          f[i-1][j-1] + f[i-1][j] , s[i] != t[j] && i >= j;
          0 , i < j;
      }
      
  • 3.确定初始状态。

    • 显然,上述转移方程中i和j的值均应该大于等于1.
    • 显然,f[0][0] = (s[0] == t[0] ? 1 : 0);
    • 显然,f[i][0] = 字符串s[0:i]中t[0]的个数。(i>0)
    • 显然,f[0][j] = 0。(j > 0)

由上述思路,可以写出AC代码:

class Solution {
public:
    int numDistinct(string s, string t) {
        int s_size = s.size();
        int t_size = t.size();
        if (s_size < t_size){  // 剪枝1
            return 0;
        }
        if (s_size == t_size){ // 剪枝2
            return s == t ? 1 : 0;
        }
        vector<vector<long long>> f(s_size, vector<long long>(t_size, 0));//注意1
        f[0][0] = (s[0] == t[0] ? 1 : 0);  
        for (int i = 1; i < s_size; i ++){
            if (s[i] == t[0]){
                f[i][0] = f[i-1][0] + 1;
            }else{
                f[i][0] = f[i-1][0];
            }
        }
        for (int j = 1; j < t_size; j ++){
            f[0][j] = 0;
        } // 这三行代码可以省略,因为f初始化为0
        for (int i = 1; i < s_size; i ++){
            for (int j = 1; j < t_size; j ++){
                if (s[i] == t[j]){
                    f[i][j] = f[i-1][j] + f[i-1][j-1];
                }else{
                    f[i][j] = f[i-1][j];
                }
            }
        }
        return f[s_size-1][t_size-1];
    }
};

执行用时:0 ms, 在所有 C++ 提交中击败了100.00%的用户
内存消耗:7.2 MB, 在所有 C++ 提交中击败了52.95%的用户

  • 有一个注意点,f中数据的类型是long long,不能是int。

    • 其实我刚开始也没注意到,提交了一次后发现报错:
      Line 27: Char 41: runtime error: signed integer overflow: 891953512 + 1474397256 cannot be represented in type 'int' (solution.cpp)
      SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior prog_joined.cpp:36:41
      
      • 也就是,超出int范围了。
    • 但是题目保证:题目数据保证答案符合 32 位带符号整数范围。
    • 先说结论:最后答案f[s_size-1][t_size-1]不一定是最大的数字。
    • 考虑一种极端的样例:s=“aab”,t=‘ac’。显然最后应该返回0。但是f[1][0] = 2。
    • 也就是说在某一次计算f[i][j]的时候,可能会超出int的范围。因此,f应该用long long。
  • 复杂度分析

    • 时间复杂度:O(s.size()*t.size()),两层for循环。
    • 空间复杂度:O(s.size()*t.size()),定义了f数组。

空间复杂度优化

转移方程:

f[i][j] = {
    f[i-1][j] , s[i] = t[j] && i >= j;
    f[i-1][j-1] + f[i-1][j] , s[i] != t[j] && i >= j;
    0 , i < j;
}
  • 可以看出,f的第一维可以优化掉。
                       j列
                       ↓
    i-1行 → ...|j-1(1)|j(2)|...
    i行   → ...|j-1(3)|j(4)|...

根据转移方程,f[i][j]只与f[i-1][j-1]和f[i][j-1]有关[也就是(4)只与(1)和(2)有关]。

因此,可以使用一维的f,AC代码如下:

class Solution {
public:
    int numDistinct(string s, string t) {
        int s_size = s.size();
        int t_size = t.size();
        if (s_size < t_size){  // 剪枝1
            return 0;
        }
        if (s_size == t_size){ // 剪枝2
            return s == t ? 1 : 0;
        }
        vector<long long> f(t_size, 0);
        f[0] = (s[0] == t[0] ? 1 : 0);  
        for (int i = 1; i < s_size; i ++){
            for (int j = t_size-1; j >= 1; j --){ // 注意2
                if (j > i){
                    f[j] = 0;
                    continue;
                } // 注意3 
                if (s[i] == t[j]){
                    f[j] = f[j] + f[j-1];
                }
            }
            if (s[i] == t[0]){
                f[0] = f[0] + 1;
            } // 注意4
        }
        return f[t_size-1];
    }
};
  • 注意2:j变量要倒序遍历!因为f[j]取决于f[j]和f[j-1],那么在确定f[j]时,f[j-1]必须是未更新的f[j-1],因此更新顺序应该是倒序。
  • 注意3:当j>i时,显然,应该f[j] = 0.
  • 注意4:因为是倒序,因此这三行代码应该放到for(j)的后面。

执行用时:0 ms, 在所有 C++ 提交中击败了100.00%的用户
内存消耗:6.2 MB, 在所有 C++ 提交中击败了97.15%的用户

  • 复杂度分析
    • 时间复杂度:O(s.size()*t.size()),两层for循环。
    • 空间复杂度:O(t.size()),定义了一维的f数组。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值