146. 字符串首尾相同子序列计数 解析 | 豆包MarsCode AI刷题

53 阅读3分钟

题目大意

给定一个由小写字母组成的字符串,询问首尾字符相同的子序列数量,并输出对 998244353998244353 取模的结果。

测试样例及解释

样例1:

输入:s = "arcaea"
输出:28

以第一位和第四位为首位的子序列共 4 个,以第一位和第六位为首位的子序列共 16 个,以第四位和第六位为首位的子序列共 2 个,长度为 1 的子序列共 6 个,一共 28 个。

样例2:

输入:s = "abcabc"
输出:18

以第一位和第四位为首位的子序列共 4 个,以第二位和第五位为首位的子序列共 4 个,以第三位和第六位为首位的子序列共 4 个,长度为 1 的子序列共 6 个,一共 18 个。

样例3:

输入:s = "aaaaa"
输出:31

任意一个子序列均符合条件,一共有 251=312^5-1=31 个。

解题思路

  1. 如果我们暴力枚举每一个子序列,时间复杂度为 O(2n)O(2^n),显然无法处理较大的数据;

  2. 我们可以发现我们只关心每一个子序列的第一个字符与最后一个字符,而对子序列中间的字符没有任何要求。因此如果两个相同字符之间存在 kk 个字符,那么这两个相同字符给答案带来的贡献就是 2k2^k,所以我们只要枚举这两个字符分别是谁就行了,时间复杂度为 O(n2)O(n^2)

  3. 但是我们并不满足于这个复杂度,能否通过进一步的优化使时间复杂度降低到 O(n)O(n) 级别呢?答案是肯定的。对于每一个字符,我们都可以预处理在其之前的相同字符可以给答案产生的贡献。具体来说,我们可以定义一个长度为 26 的数组 cntcnt,数组中的第 i 个记录字母表中第 i 个此时的贡献;每多一个字符,整个 cntcnt 数组都会整体乘二,因为我们既可以选择加入这个字符也可以选择不加入这个字符;同时,cnticnt_i 的值需要自增一,因为该字符又出现了一个新的起点。于是,我们每次就可以通过累加当前字符对应 cntcnt 数组中的哪一个统计答案。但需要注意统计答案需要在更新 cntcnt 数组之前;cnticnt_i的自增要在更新 cntcnt 数组之后。

  4. 然后我们运行程序,就会发现我们的答案刚好都比正确答案少了字符串的长度——长度为一的子序列我们没有统计到。因此最后给答案加上字符串的长度即可。

int solution(std::string s) {
  for (int i = 0; i < N; i++)
    cnt[i] = 0;
  long long ans = s.size(); // 初始化 ans 为字符串长度

  for (auto c : s) {
    // 更新 ans,考虑当前字符 c 作为子序列的结尾
    ans = (ans + cnt[c - 'a']) % M; //这里计算当前字符 c 作为结尾的子序列数量 

    // 更新 cnt 数组,考虑当前字符 c 的组合方式
    for (int i = 0; i < N; i++)
      cnt[i] = cnt[i] * 2 % M;
    cnt[c - 'a']++;
  }

  return ans;
}