力扣每日一题0610-730. 统计不同回文子序列

132 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第10天,点击查看活动详情

给定一个字符串 s,返回 s 中不同的非空「回文子序列」个数 。

通过从 s 中删除 0 个或多个字符来获得子序列。

如果一个字符序列与它反转后的字符序列一致,那么它是「回文字符序列」。

如果有某个 i , 满足 ai != bi ,则两个序列 a1, a2, ... 和 b1, b2, ... 不同。

注意:

  • 结果可能很大,你需要对 109 + 7 取模 。

示例 1:

输入:s = 'bccb'
输出:6
解释:6 个不同的非空回文子字符序列分别为:'b', 'c', 'bb', 'cc', 'bcb', 'bccb'。
注意:'bcb' 虽然出现两次但仅计数一次。

示例 2:

输入:s = 'abcdabcdabcdabcdabcdabcdabcdabcddcbadcbadcbadcbadcbadcbadcbadcba'
输出:104860361
解释:共有 3104860382 个不同的非空回文子序列,104860361 对 109 + 7 取模后的值。

动态规划(使用三维数组)

显然每一个「回文序列」都满足开头和结尾的字符相同。那么我们设给定字符串为 ss,长度为 nn,状态 dp(x,i,j)\textit{dp}(x,i,j) 表示在字符串区间 s[i:j]s[i:j] 中以字符 xx 为开头和结尾的不同「回文序列」总数,其中 s[i:j]s[i:j] 表示字符串 ss 从下标 ii 到下标 jj 的子串(包含下标 ii 和下标 jj)。那么最终我们需要求的答案就转化为了 (i=0Cdp(xi,0,n1))mod1000000007(\sum_{i=0}^C\textit{dp}(x_i,0,n-1)) \bmod 1000000007,其中 xiSx_i \in SSS 为题目给定的的字符集合,CC 为该字符集合的大小。

  1. s[i]=xs[i]=xs[j]=xs[j]=x 时,那么对于 s[i+1:j1]s[i+1:j-1] 中的任意一个「回文序列」在头尾加上字符 xx 都会生成一个新的以字符 xx 为开头结尾的「回文序列」,并加上 xxxxxx 两个单独的「回文序列」。下式中,由于 xkx_k 不同的「回文序列」一定互不相同,因此可以直接累加,无需考虑去重。

    dp(x,i,j)=2+k=0Cdp(xk,i+1,j1)dp(x,i,j)=2+\sum_{k=0}^Cdp(x_k,i+1,j−1)

  2. s[i]=xs[i]=xs[j]xs[j] \neq x 时,那么 dp(x,i,j)\textit{dp}(x,i,j) 等价于 dp(x,i,j1)\textit{dp}(x,i,j-1)

    dp(x,i,j)=dp(x,i,j1)dp(x,i,j)=dp(x,i,j−1)

  3. s[i]xs[i] \neq xs[j]=xs[j]=x 时,那么 dp(x,i,j)\textit{dp}(x,i,j) 等价于 dp(x,i+1,j)\textit{dp}(x,i+1,j)

    dp(x,i,j)=dp(x,i+1,j)dp(x,i,j)=dp(x,i+1,j)

  4. s[i]xs[i] \neq xs[j]xs[j] \neq x 时,那么 dp(x,i,j)\textit{dp}(x,i,j) 等价于 dp(x,i+1,j1)\textit{dp}(x,i+1,j-1)

    dp(x,i,j)=dp(x,i+1,j1)dp(x,i,j)=dp(x,i+1,j−1)

上文的讨论是建立在字符串长度大于 1\text{1} 的前提上的,我们还需要考虑动态规划的边界条件,即字符串长度为 1\text{1} 或者不存在的情况。对于长度为 1\text{1} 的字符串,它显然只有本身这一个「回文序列」。对于字符串不存在的情况,那么肯定不存在任何「回文序列」子串。因此我们就可以写出动态规划的边界条件:

image.png 可以看到每一个区间上的求解都与其小区间的求解有关,所以我们可以采用「自底向上」的编码方式来实现求解过程。最终返回 (i=0Cdp(xi,0,n1))mod1000000007\sum_{i=0}^C\textit{dp}(x_i,0,n-1)) \bmod 1000000007 即可。

var countPalindromicSubsequences = function(s) {
    const MOD = 1000000007;
    const n = s.length;
    const dp = new Array(4).fill(0).map(() => new Array(n).fill(0).map(() => new Array(n).fill(0)));
    for (let i = 0; i < n; i++) {
        dp[s[i].charCodeAt() - 'a'.charCodeAt()][i][i] = 1;
    }

    for (let len = 2; len <= n; len++) {
        for (let i = 0; i + len <= n; i++) {
            let j = i + len - 1;
            for (const c of ['a', 'b', 'c', 'd']) {
                const k = c.charCodeAt() - 'a'.charCodeAt();
                if (s[i] === c && s[j] === c) {
                    dp[k][i][j] = (2 + (dp[0][i + 1][j - 1] + dp[1][i + 1][j - 1]) % MOD + (dp[2][i + 1][j - 1] + dp[3][i + 1][j - 1]) % MOD) % MOD;
                } else if (s[i] === c) {
                    dp[k][i][j] = dp[k][i][j - 1];
                } else if (s[j] === c) {
                    dp[k][i][j] = dp[k][i + 1][j];
                } else {
                    dp[k][i][j] = dp[k][i + 1][j - 1];
                }
            }
        }
    }

    let res = 0;
    for (let i = 0; i < 4; i++) {
        res = (res + dp[i][0][n - 1]) % MOD;
    }
    return res;
};