【940. 不同的子序列 II】

来源:力扣(LeetCode)

链接:

  给定一个字符串 s,计算 s 的 不同非空子序列 的个数。因为结果可能很大,所以返回答案需要对 109 + 7 取余

  字符串的 子序列 是经由原字符串删除一些(也可能不删除)字符但不改变剩余字符相对位置的一个新字符串。

  • 例如,“ace” 是 “abcde” 的一个子序列,但 “aec” 不是。

示例 1:

输入:s = "abc"
输出:7
解释:7 个不同的子序列分别是 "a", "b", "c", "ab", "ac", "bc", 以及 "abc"

示例 2:

输入:s = "aba"
输出:6
解释:6 个不同的子序列分别是 "a", "b", "ab", "ba", "aa" 以及 "aba"

示例 3:

输入:s = "aaa"
输出:3
解释:3 个不同的子序列分别是 "a", "aa" 以及 "aaa"

提示:

  • 1 <= s.length <= 2000
  • s 仅由小写英文字母组成

方法一:动态规划

思路与算法

  我们用 f[i] 表示以 s[i] 为最后一个字符的子序列的数目。

  • 如果子序列中只有 s[i] 这一个字符,那么有一种方案;

  • 如果子序列中至少有两个字符,那么我们可以枚举倒数第二个字符来进行状态转移。容易想到的是:倒数第二个字符可以选择 s[0], s[1] , ⋯ , s[i−1] 中的某一个,这样就可以得到如下的状态转移方程:

f[i] = f[0] + f[1] + ⋯ f[i−1]

  然而这样做会产生重复计数。如果 s[j0] 和 s[j1] 这两个字符不相同,那么 f[j0] 和 f[j1] 对应的子序列是两个不相交的集合;但如果 s[j0] 和 s[j1] 这两个字符相同,那么 f[j0] 和 f[j1] 对应的子序列会包含重复的项。最简单的一个重复项就是只含有一个字符的子序列 s[j0] 和 s[j1] 本身。

  那么我们该如何防止重复计数呢?可以发现,如果 j0 < j1,那么 f[j0] 一定是 f[j1] 的一个真子集。这是因为:

每一个以 s[j0] 为最后一个字符的子序列,都可以把这个字符改成完全相同的 s[j1] ,计入到 f[j1] 中。

  因此,对于每一种字符,我们只需要找到其最后一次出现的位置(并且在位置 i 之前),并将对应的 f 值累加进 f[i] 即可。由于本题中字符串只包含小写字母,我们可以用 ]last[k] 记录第 k (0 ≤ k < 26) 个小写字母最后一次出现的位置。如果它还没有出现过,那么 last[k] = −1。这样我们就可以写出正确的状态转移方程:

1
  将这两种情况合并在一起,最终的状态转移方程即为:
2
  在计算完 f[i] 后,我们需要记得更新对应的 last 项。最终的答案即为:
3
代码:

class Solution {
public:
    int distinctSubseqII(string s) {
        vector<int> last(26, -1);
        
        int n = s.size();
        vector<int> f(n, 1);
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < 26; ++j) {
                if (last[j] != -1) {
                    f[i] = (f[i] + f[last[j]]) % mod;
                }
            }
            last[s[i] - 'a'] = i;
        }
        
        int ans = 0;
        for (int i = 0; i < 26; ++i) {
            if (last[i] != -1) {
                ans = (ans + f[last[i]]) % mod;
            }
        }
        return ans;
    }

private:
    static constexpr int mod = 1000000007;
};

执行用时:16 ms, 在所有 C++ 提交中击败了12.45%的用户
内存消耗:6.6 MB, 在所有 C++ 提交中击败了35.85%的用户
复杂度分析
时间复杂度: O(n∣Σ∣),其中 n 是字符串 s 的长度, Σ 是字符集,在本题中 ∣Σ∣=26。即为动态规划需要的时间。
空间复杂度: O(n+∣Σ∣)。即为数组 f 和 last 需要的空间。

方法二:优化的动态规划

思路与算法

  观察方法一中的状态转移方程:

4
  我们可以考虑使用一个长度为 Σ∣=26 的数组 g 来进行动态规划,其中 g[k] 就表示上述状态转移方程中的 f[last[k]]。记 oi 表示 s[i] 是第 oi 个字母,我们可以在遍历到 s[i] 时,更新 g[oi] 的值为:
5
  即可。当 last[k] = −1 时我们无需进行转移,那么只要将数组 g 的初始值设为 0,在累加时就可以达到相同的效果。

代码:

class Solution {
public:
    int distinctSubseqII(string s) {
        vector<int> g(26, 0);
        int n = s.size();
        for (int i = 0; i < n; ++i) {
            int total = 1;
            for (int j = 0; j < 26; ++j) {
                total = (total + g[j]) % mod;
            }
            g[s[i] - 'a'] = total;
        }
        
        int ans = 0;
        for (int i = 0; i < 26; ++i) {
            ans = (ans + g[i]) % mod;
        }
        return ans;
    }

private:
    static constexpr int mod = 1000000007;
};

执行用时:8 ms, 在所有 C++ 提交中击败了30.19%的用户
内存消耗:6.3 MB, 在所有 C++ 提交中击败了78.11%的用户
复杂度分析
时间复杂度: O(n∣Σ∣),其中 n 是字符串 s 的长度, Σ 是字符集,在本题中 ∣Σ∣=26。即为动态规划需要的时间。
空间复杂度: O(∣Σ∣)。即为数组 g 和 last 需要的空间。

方法三:继续优化的动态规划

思路与算法

  观察方法二中的状态转移方程:

6
  由于我们的答案是数组 g 的和,而遍历 s[i] 后只有 g[oi] 的值发生了变化。因此我们可以使用一个变量 total 直接维护数组 g 的和,每次将 g[oi] 的值更新为 1 + total,再将 total 的值增加 g[oi] 的变化量即可。

代码:

class Solution {
public:
    int distinctSubseqII(string s) {
        vector<int> g(26, 0);
        int n = s.size(), total = 0;
        for (int i = 0; i < n; ++i) {
            int oi = s[i] - 'a';
            int prev = g[oi];
            g[oi] = (total + 1) % mod;
            total = ((total + g[oi] - prev) % mod + mod) % mod;
        }
        return total;
    }

private:
    static constexpr int mod = 1000000007;
};

执行用时:4 ms, 在所有 C++ 提交中击败了78.49%的用户
内存消耗:6.2 MB, 在所有 C++ 提交中击败了94.72%的用户
复杂度分析
时间复杂度: O(n+∣Σ∣)。其中 n 是字符串 s 的长度, Σ 是字符集,在本题中 ∣Σ∣=26。初始化需要的时间为 O(∣Σ∣),动态规划需要的时间的为 O(n)。
空间复杂度: O(∣Σ∣)。即为数组 g 需要的空间。
author:LeetCode-Solution

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

千北@

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值