持续创作,加速成长!这是我参与「掘金日新计划 · 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 取模后的值。
动态规划(使用三维数组)
显然每一个「回文序列」都满足开头和结尾的字符相同。那么我们设给定字符串为 ,长度为 ,状态 表示在字符串区间 中以字符 为开头和结尾的不同「回文序列」总数,其中 表示字符串 从下标 到下标 的子串(包含下标 和下标 )。那么最终我们需要求的答案就转化为了 ,其中 , 为题目给定的的字符集合, 为该字符集合的大小。
-
当 且 时,那么对于 中的任意一个「回文序列」在头尾加上字符 都会生成一个新的以字符 为开头结尾的「回文序列」,并加上 和 两个单独的「回文序列」。下式中,由于 不同的「回文序列」一定互不相同,因此可以直接累加,无需考虑去重。
-
当 且 时,那么 等价于 。
-
当 且 时,那么 等价于 。
-
当 且 时,那么 等价于 。
上文的讨论是建立在字符串长度大于 的前提上的,我们还需要考虑动态规划的边界条件,即字符串长度为 或者不存在的情况。对于长度为 的字符串,它显然只有本身这一个「回文序列」。对于字符串不存在的情况,那么肯定不存在任何「回文序列」子串。因此我们就可以写出动态规划的边界条件:
可以看到每一个区间上的求解都与其小区间的求解有关,所以我们可以采用「自底向上」的编码方式来实现求解过程。最终返回 ( 即可。
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;
};