Bootstrap

求解固定长度的回文子序列数目

前言: 有半年时间没有写博客了,今天写题目的时候遇到了一个之前完全没有见过的题目,因此前来记录一下。


问题描述

  • 对于字符串 s s s,需要计算其中长度为 k k k回文子序列 数目,通常 1 < k ≤ 5 1 < k \le 5 1<k5

1. k = 2 k = 2 k=2

  • 先简化问题,当 k = 2 k = 2 k=2 时,回文子序列的个数即为 s s s 中的相同字符对,采用一次遍历便可以完成计算
long long solve(string s){
	int cnt[26] = {0};
	long long ans = 0;
	for(char c : s){
		int x = c - 'a';
		ans += cnt[x];
		cnt[x]++;
	}
	return ans;
}
  • 时间复杂度: O ( n ) O(n) O(n) n n n 为字符串 s s s 的长度。

2. k = 3 k = 3 k=3

  • 进一步考虑 k = 3 k = 3 k=3 的情况,由于 k k k 为奇数,枚举 s s s 中的每一个字符 c c c 作为中心字符,同时枚举第一个字符的可能取值。
  • 实现细节:当下标为 i i i 时,需要考虑 i i i 的左右两部分,即分别进行 前缀后缀 计数。首先逆向遍历 s s s,统计每个字符出现的数目 s u f [ a ] suf[a] suf[a],再正向遍历,统计 p r e [ a ] pre[a] pre[a] 的同时更新 s u f suf suf,实现前后缀同步。
  • 题目示范L.C. 1930. 长度为 3 的不同回文子序列
class Solution {
public:
    int countPalindromicSubsequence(string s) {
        int n = s.size();
        int suf[26] = {0}, pre[26] = {0};
        for(int i = 0; i < n; ++i){
            suf[s[i] - 'a']++;
        }
        int ans[26][26];
        memset(ans, 0, sizeof(ans));
        
        for(int i = 0; i < n; ++i){
            int x = s[i] - 'a';
            suf[x]--;
            for(int j = 0; j < 26; ++j){
                if(pre[j] > 0 && suf[j] > 0)
                    ans[x][j] = 1;
            }
            pre[x]++;
        }
        int ret = 0;
        for(int i = 0; i < 26; ++i){
            for(int j = 0; j < 26; ++j){
                ret += ans[i][j];
            }
        }
        return ret;
    }
};
  • 时间复杂度: O ( n ∣ Σ ∣ ) O(n \left | \Sigma \right |) O(nΣ) n n n 为字符串 s s s 的长度, ∣ Σ ∣ \left | \Sigma \right | Σ 为字符集大小

3. k ∈ [ 4 , 5 ] k \in [4, 5] k[4,5]

  • 难度升级,考虑 k ∈ [ 4 , 5 ] k \in [4, 5] k[4,5] 的情况。对于 k ∈ [ 4 , 5 ] k \in [4, 5] k[4,5],可以分别从 k = 2 k = 2 k=2 k = 3 k = 3 k=3 的情况实现进一步递推。

  • 对于 k ∈ [ 2 , 3 ] k \in [2, 3] k[2,3],我们枚举右边的一个字符,因此对于 k ∈ [ 4 , 5 ] k \in [4, 5] k[4,5],我们需要枚举右边的两个字符。

  • 实现细节

    • 首先逆向遍历 s s s,统计每一个字符出现的数目 s u f [ a ] suf[a] suf[a] 和两个字符的组合数目 s u f 2 [ a ] [ b ] suf2[a][b] suf2[a][b]
    • 再正向遍历 s s s,同样维护两个数组 p r e [ a ] pre[a] pre[a] p r e 2 [ a ] [ b ] pre2[a][b] pre2[a][b],并在遍历过程中枚举两个字符的所有组合。
  • 题目示范L.C. 6251. 统计回文子序列数目

class Solution {
public:
    const int mod = 1e9 + 7;

    int suf2[10][10], suf[10];
    int pre2[10][10], pre[10];
    
    int countPalindromes(string s) {
        int n = s.size();
        
        for(int i = n - 1; i >= 0; --i){
            int x = s[i] - '0';
            for(int j = 0; j < 10; ++j){
                suf2[x][j] = (suf2[x][j] + suf[j]) % mod;
            }
            suf[x]++;
        }
        
        int ans = 0;
        for(char c : s){
            int x = c - '0';
            // 撤销后缀
            suf[x]--;
            for(int j = 0; j < 10; ++j){
                suf2[x][j] = (suf2[x][j] - suf[j] + mod) % mod;
            }
            // 枚举两个字符的所有可能情况
            for(int j = 0; j < 10; ++j){
                for(int k = 0; k < 10; ++k){
                    ans = (ans + (1LL * pre2[j][k] * suf2[j][k]) % mod) % mod;
                }
            }
            // 计算前缀
            for(int j = 0; j < 10; ++j){
                pre2[x][j] = (pre2[x][j] + pre[j]) % mod;
            }
            pre[x]++;
        }      
        return ans;
    }
};
  • 时间复杂度: O ( n ∣ Σ ∣ 2 ) O(n \left | \Sigma \right |^2) O(nΣ2) n n n 为字符串 s s s 的长度, ∣ Σ ∣ \left | \Sigma \right | Σ 为字符集大小

  • 注意:对于 k = 4 k = 4 k=4 k = 5 k = 5 k=5 虽然在算法的具体思路上是相似的,但是在实现细节上有些许不同:

    • 对于 k = 5 k = 5 k=5,需要枚举中心字符,因此当遍历到下标 i i i 时,需要先撤销后缀,再枚举字符组合,最后更新前缀,总是仅覆盖 n − 1 n - 1 n1 个字符。
    • 对于 k = 4 k = 4 k=4,由于不存在中心字符,因此可以 在撤销后缀的同时更新前缀,再枚举字符组合,覆盖 n n n 个字符。
  • k = 4 k = 4 k=4 的部分代码

 for(char c : s){
    int x = c - '0';
    // 撤销后缀 + 更新前缀
    suf[x]--;
    for(int j = 0; j < 10; ++j){
        suf2[x][j] = (suf2[x][j] - suf[j] + mod) % mod;
        pre2[x][j] = (pre2[x][j] + pre[j]) % mod;
    }
    pre[x]++;
    // 枚举两个字符的所有可能情况
    for(int j = 0; j < 10; ++j){
        for(int k = 0; k < 10; ++k){
            ans = (ans + (1LL * pre2[j][k] * suf2[j][k]) % mod) % mod;
        }
    }
}      
;