青训营伴学笔记丨MarsCode刷题日记丨不同子序列计数问题

83 阅读3分钟

问题背景

小U有一个字符串 s,他想计算该字符串的所有不同非空子序列的个数。子序列是通过删除原字符串中的部分字符(也可以不删除),且保持剩余字符的相对顺序形成的新字符串。

你的任务是帮助小U计算 s 的不同非空子序列的总数,并返回对 10^9 + 7 取余的结果。

例如:当 s = "abc" 时,所有不同的非空子序列包括 "a""b""c""ab""ac""bc", 和 "abc",总数为 7。

测试样例

样例1:

输入:s = "abc"
输出:7

样例2:

输入:s = "aaa"
输出:3

样例3:

输入:s = "abcd"
输出:15

样例4:

输入:s = "abac"
输出:13

问题理解

我们需要计算字符串 s 的所有不同非空子序列的个数。子序列是通过删除原字符串中的部分字符(也可以不删除),且保持剩余字符的相对顺序形成的新字符串。

数据结构选择

我们可以使用动态规划(Dynamic Programming, DP)来解决这个问题。动态规划可以帮助我们高效地计算所有可能的子序列,并避免重复计算。

算法步骤

  1. 定义状态

    • 设 dp[i] 表示前 i 个字符可以形成的不同子序列的个数。
    • 初始状态:dp[0] = 1,表示空字符串也算一个子序列。
  2. 状态转移

    • 对于每个字符 s[i],我们需要考虑两种情况:

      • 不包含 s[i] 的子序列,这部分的数量是 dp[i-1]
      • 包含 s[i] 的子序列,这部分的数量是 dp[i-1] 加上所有以 s[i] 结尾的子序列。
    • 为了避免重复计算,我们可以使用一个哈希表 last_seen 来记录每个字符最后一次出现的位置。

  3. 最终结果

    • 最终结果是 dp[n] - 1,其中 n 是字符串的长度,减去 1 是因为要去掉空字符串的情况。
def solution(s: str) -> int:
    MOD = 10**9 + 7
    n = len(s)
    
    # 初始化 dp 数组
    dp = [0] * (n + 1)
    dp[0] = 1  # 空字符串也算一个子序列
    
    # 记录每个字符最后一次出现的位置
    last_seen = {}
    
    for i in range(1, n + 1):
        dp[i] = dp[i - 1] * 2 % MOD
        
        # 如果当前字符之前出现过,减去重复的部分
        if s[i - 1] in last_seen:
            dp[i] = (dp[i] - dp[last_seen[s[i - 1]] - 1] + MOD) % MOD
        
        # 更新当前字符的最后出现位置
        last_seen[s[i - 1]] = i
    
    # 返回结果,减去空字符串的情况
    return (dp[n] - 1 + MOD) % MOD

关键步骤解释

  • dp[i] = dp[i - 1] * 2 % MOD:表示不包含当前字符和包含当前字符的子序列总数。
  • if s[i - 1] in last_seen:检查当前字符是否之前出现过,如果出现过,则需要减去重复的部分。
  • last_seen[s[i - 1]] = i:更新当前字符的最后出现位置。