leetcode
leetcode 851 ~ 900
不同的子序列 II

不同的子序列 II

难度:

标签:

题目描述

代码结果

运行时间: 36 ms, 内存: 16.0 MB


/*
 * 思路:
 * 1. 使用动态规划结合Java Stream API。
 * 2. 用一个数组dp记录前i个字符的不同非空子序列个数。
 * 3. 使用一个数组last来记录每个字符上一次出现的位置。
 * 4. 最终答案需要对10^9 + 7取余。
 */
import java.util.stream.IntStream;
public class Solution {
    public int distinctSubseqII(String s) {
        int MOD = 1000000007;
        int n = s.length();
        int[] dp = new int[n + 1];
        int[] last = new int[26];
        dp[0] = 1;
        IntStream.range(1, n + 1).forEach(i -> {
            char c = s.charAt(i - 1);
            dp[i] = (dp[i - 1] * 2 % MOD - (last[c - 'a'] > 0 ? dp[last[c - 'a'] - 1] : 0) + MOD) % MOD;
            last[c - 'a'] = i;
        });
        return dp[n] - 1;
    }
}

解释

方法:

这个题解使用了动态规划的思想来计算所有的不同子序列。具体方法是用一个数组endCnt来记录以每个字符结尾的子序列的数量。对于字符串s中的每个字符x,当前所有的子序列可以与x组合形成新的子序列,而新形成的以x结尾的子序列的数量就是在x加入之前的所有子序列数量减去上一次以x结尾的子序列数量。这样可以保证计数的过程中不会重复计算相同的子序列。最终结果需要减去初始化时候计入的空子序列。

时间复杂度:

O(n)

空间复杂度:

O(1)

代码细节讲解

🦆
为什么初始化ans为1而不是0?这是否代表已经计入了空子序列?
是的,初始化ans为1而不是0是因为在动态规划的过程中已经考虑了空子序列。空子序列是任何字符串的有效子序列之一,因此在开始计算时将其包含进总数。这样做可以简化后续的计算,因为每个新字符引入时,都可以默认存在与空子序列结合成新子序列的情形。
🦆
在更新endCnt数组时,为何需要从ans减去上一次以该字符结尾的子序列数量?这样的操作有什么特别的意义或目的?
这样做的目的是为了避免重复计数。在动态规划过程中,每个字符x加入到现有的所有子序列中会形成新的子序列,但如果直接加上前面所有的子序列总数,会重复计算以x结尾的子序列。通过减去上一次以该字符x结尾的子序列数量,我们确保每次加入的都是新生成的、独特的子序列,而不包括之前已经计算过的以x结尾的子序列。
🦆
题解中提到,为了避免重复计算相同的子序列,需要进行某些减法操作。这个逻辑是如何确保算法的正确性的?
通过减去以当前字符结尾的上一次的子序列数量,我们确保每次添加的子序列都是基于当前字符新形成的。这意味着每个新形成的子序列都是从当前字符和之前计算的所有独立子序列组合而成,而不包括任何之前已经存在的以该字符结尾的子序列。这种方法确保了算法在更新子序列计数时不会出现重复,从而保持了算法的正确性。
🦆
题解最后返回的结果为何要进行(ans - 1 + mod) % mod这样的操作?直接返回ans - 1不行吗?
直接返回ans - 1在大多数情况下是没有问题的。然而考虑到ans可能为0(尤其是在空字符串或者其他特殊情况下),直接做减法可能导致结果为负数。在编程实现中,返回负数可能不符合预期的输出要求。通过添加mod并取模,确保最终结果始终为非负整数,并且在处理大数时防止溢出,符合模运算的常规操作。

相关问题