Hard
题目描述
给定一个字符串 s,返回 s 中不同的非空回文子序列个数。由于答案可能很大,返回答案对 109 + 7 取模的结果。
字符串的子序列是通过删除字符串中零个或多个字符而得到的。
如果一个序列与它的反转相等,那么它是回文的。
如果存在某个 i 使得 ai != bi,那么两个序列 a1, a2, ... 和 b1, b2, ... 不同。
示例 1:
输入:s = "bccb"
输出:6
解释:6 个不同的非空回文子序列分别为:'b', 'c', 'bb', 'cc', 'bcb', 'bccb'。
注意:'bcb' 只能被计算一次,即使它出现了两次。
示例 2:
输入:s = "abcdabcdabcdabcdabcdabcdabcdabcddcbadcbadcbadcbadcbadcbadcbadcba"
输出:104860361
解释:总共有 3104860382 个不同的非空回文子序列,取模 109 + 7 后为 104860361。
提示:
1 <= s.length <= 1000s[i]是'a'、'b'、'c'或'd'中的一个
解题思路
这是一道经典的动态规划题目,核心思想是利用区间 DP 来计算每个子串中不同回文子序列的数量。
基本思路:
设 dp[i][j] 表示字符串 s[i...j] 中不同回文子序列的数量。
状态转移:
对于每个字符 c(‘a’、‘b’、‘c’、’d’),我们需要考虑以 c 开头和结尾的回文子序列:
- 如果区间
[i,j]内没有字符c,那么以c开头结尾的回文子序列数量为 0 - 如果区间
[i,j]内只有一个字符c,那么只有一个回文子序列:单个字符c - 如果区间
[i,j]内有多个字符c,设最左边的c在位置left,最右边的c在位置right,那么以c开头结尾的回文子序列数量等于dp[left+1][right-1] + 2(加2是因为单个c和两个c组成的cc)
优化细节:
- 预处理每个位置左右最近的相同字符位置,避免重复计算
- 使用模运算防止溢出
- 区间长度从小到大进行动态规划
时间复杂度为 O(n²),空间复杂度为 O(n²),其中 n 是字符串长度。
代码实现
class Solution {
public:
int countPalindromicSubsequences(string s) {
int n = s.length();
const int MOD = 1000000007;
// dp[i][j] 表示 s[i...j] 中不同回文子序列的数量
vector<vector<int>> dp(n, vector<int>(n, 0));
// 预处理:找到每个位置左边和右边最近的相同字符位置
vector<vector<int>> next(n, vector<int>(4, n));
vector<vector<int>> prev(n, vector<int>(4, -1));
// 从右到左扫描,计算 next 数组
for (int i = n - 1; i >= 0; i--) {
if (i < n - 1) {
for (int j = 0; j < 4; j++) {
next[i][j] = next[i + 1][j];
}
}
next[i][s[i] - 'a'] = i;
}
// 从左到右扫描,计算 prev 数组
for (int i = 0; i < n; i++) {
if (i > 0) {
for (int j = 0; j < 4; j++) {
prev[i][j] = prev[i - 1][j];
}
}
prev[i][s[i] - 'a'] = i;
}
// 动态规划
for (int len = 1; len <= n; len++) {
for (int i = 0; i <= n - len; i++) {
int j = i + len - 1;
for (int c = 0; c < 4; c++) {
int left = next[i][c];
int right = (j >= 0) ? prev[j][c] : -1;
if (left <= j && right >= i) {
if (left == right) {
dp[i][j] = (dp[i][j] + 1) % MOD;
} else {
dp[i][j] = (dp[i][j] + dp[left + 1][right - 1] + 2) % MOD;
}
}
}
}
}
return dp[0][n - 1];
}
};
class Solution:
def countPalindromicSubsequences(self, s: str) -> int:
n = len(s)
MOD = 10**9 + 7
# dp[i][j] 表示 s[i:j+1] 中不同回文子序列的数量
dp = [[0] * n for _ in range(n)]
# 预处理:找到每个位置左边和右边最近的相同字符位置
next_pos = [[n] * 4 for _ in range(n)]
prev_pos = [[-1] * 4 for _ in range(n)]
# 从右到左扫描,计算 next_pos 数组
for i in range(n - 1, -1, -1):
if i < n - 1:
for j in range(4):
next_pos[i][j] = next_pos[i + 1][j]
next_pos[i][ord(s[i]) - ord('a')] = i
# 从左到右扫描,计算 prev_pos 数组
for i in range(n):
if i > 0:
for j in range(4):
prev_pos[i][j] = prev_pos[i - 1][j]
prev_pos[i][ord(s[i]) - ord('a')] = i
# 动态规划
for length in range(1, n + 1):
for i in range(n - length + 1):
j = i + length - 1
for c in range(4):
left = next_pos[i][c]
right = prev_pos[j][c] if j >= 0 else -1
if left <= j and right >= i:
if left == right:
dp[i][j] = (dp[i][j] + 1) % MOD
else:
dp[i][j] = (dp[i][j] + dp[left + 1][right - 1] + 2) % MOD
return dp[0][n - 1]
public class Solution {
public int CountPalindromicSubsequences(string s) {
int n = s.Length;
const int MOD = 1000000007;
// dp[i][j] 表示 s[i...j] 中不同回文子序列的数量
int[,] dp = new int[n, n];
// 预处理:找到每个位置左边和右边最近的相同字符位置
int[,] next = new int[n, 4];
int[,] prev = new int[n, 4];
// 初始化 next 数组
for (int i = 0; i < n; i++) {
for (int j = 0; j < 4; j++) {
next[i, j] = n;
prev[i, j] = -1;
}
}
// 从右到左扫描,计算 next 数组
for (int i = n - 1; i >= 0; i--) {
if (i < n - 1) {
for (int j = 0; j < 4; j++) {
next[i, j] = next[i + 1, j];
}
}
next[i, s[i] - 'a'] = i;
}
// 从左到右扫描,计算 prev 数组
for (int i = 0; i < n; i++) {
if (i > 0) {
for (int j = 0; j < 4; j++) {
prev[i, j] = prev[i - 1, j];
}
}
prev[i, s[i] - 'a'] = i;
}
// 动态规划
for (int len = 1; len <= n; len++) {
for (int i = 0; i <= n - len; i++) {
int j = i + len - 1;
for (int c = 0; c < 4; c++) {
int left = next[i, c];
int right = (j >= 0) ? prev[j, c] : -1;
if (left <= j && right >= i) {
if (left == right) {
dp[i, j] = (dp[i, j] + 1) % MOD;
} else {
dp[i, j] = (dp[i, j] + dp[left + 1, right - 1] + 2) % MOD;
}
}
}
}
}
return dp[0, n - 1];
}
}
var countPalindromicSubsequences = function(s) {
const MOD = 1000000007;
const n = s.length;
const memo = {};
function dp(i, j, c) {
if (i > j) return 0;
const key = `${i}-${j}-${c}`;
if (key in memo) return memo[key];
let result = 0;
if (c === '') {
for (let ch of 'abcd') {
result = (result + dp(i, j, ch)) % MOD;
}
} else {
let left = -1, right = -1;
for (let k = i; k <= j; k++) {
if (s[k] === c) {
if (left === -1) left = k;
right = k;
}
}
if (left === -1) {
result = 0;
} else if (left === right) {
result = 1;
} else {
result = 2;
for (let ch of 'abcd') {
result = (result + dp(left + 1, right - 1, ch)) % MOD;
}
}
}
return memo[key] = result;
}
return dp(0, n - 1, '');
};
复杂度分析
| 复杂度类型 | 值 | 说明 |
|---|---|---|
| 时间复杂度 | O(n²) | 需要填充 n×n 的 dp 数组,每个位置需要遍历 4 个字符 |
| 空间复杂度 | O(n²) | dp 数组、next 数组和 prev 数组的空间开销 |