原文链接: https://leetcode-cn.com/problems/count-different-palindromic-subsequences
英文原文
Given a string s, return the number of different non-empty palindromic subsequences in s
. Since the answer may be very large, return it modulo 109 + 7
.
A subsequence of a string is obtained by deleting zero or more characters from the string.
A sequence is palindromic if it is equal to the sequence reversed.
Two sequences a1, a2, ...
and b1, b2, ...
are different if there is some i
for which ai != bi
.
Example 1:
Input: s = "bccb" Output: 6 Explanation: The 6 different non-empty palindromic subsequences are 'b', 'c', 'bb', 'cc', 'bcb', 'bccb'. Note that 'bcb' is counted only once, even though it occurs twice.
Example 2:
Input: s = "abcdabcdabcdabcdabcdabcdabcdabcddcbadcbadcbadcbadcbadcbadcbadcba" Output: 104860361 Explanation: There are 3104860382 different non-empty palindromic subsequences, which is 104860361 modulo 109 + 7.
Constraints:
1 <= s.length <= 1000
s[i]
is either'a'
,'b'
,'c'
, or'd'
.
中文题目
给定一个字符串 S,找出 S 中不同的非空回文子序列个数,并返回该数字与 10^9 + 7
的模。
通过从 S 中删除 0 个或多个字符来获得子序列。
如果一个字符序列与它反转后的字符序列一致,那么它是回文字符序列。
如果对于某个 i
,A_i != B_i
,那么 A_1, A_2, ...
和 B_1, B_2, ...
这两个字符序列是不同的。
示例 1:
输入: S = 'bccb' 输出:6 解释: 6 个不同的非空回文子字符序列分别为:'b', 'c', 'bb', 'cc', 'bcb', 'bccb'。 注意:'bcb' 虽然出现两次但仅计数一次。
示例 2:
输入: S = 'abcdabcdabcdabcdabcdabcdabcdabcddcbadcbadcbadcbadcbadcbadcbadcba' 输出:104860361 解释: 共有 3104860382 个不同的非空回文子序列,对 10^9 + 7 取模为 104860361。
提示:
- 字符串
S
的长度将在[1, 1000]
范围内。 - 每个字符
S[i]
将会是集合{'a', 'b', 'c', 'd'}
中的某一个。
通过代码
官方题解
方法一:动态规划(使用三维数组)
算法:
定义 dp[x][i][j]
为子串 S[i...j]
拥有不同回文子字符串的答案,且 S[i] == S[j] == 'a'+x
。由于字符串只包含四个字符 a, b, c, d
,因此 0 <= x < 4
。dp 的公式如下:
- 如果
S[i] != 'a'+x
,则dp[x][i][j] = dp[x][i+1][j]
- 如果
S[j] != 'a'+x
,则dp[x][i][j] = dp[x][i][j-1]
- 如果
S[i] == S[j] == 'a'+x
,则dp[x][i][j] = 2 + dp[0][i+1][j-1] + dp[1][i+1][j-1] + dp[2][i+1][j-1] + dp[3][i+1][j-1]
。当第一个和最后一个字符相同时,我们需要计算子串S[i+1][j-1]
中所有不同的回文(a、b、c、d 中的每一个)加上第一个和最后一个字符的两个回文。
设n
为字符串S
的长度,则最终的答案为dp[0][0][n-1] + dp[1][0][n-1] + dp[2][0][n-1] + dp[3][0][n-1] mod 1000000007
class Solution {
public:
int countPalindromicSubsequences(string S) {
int n = S.size();
int mod = 1000000007;
auto dp_ptr = new vector<vector<vector<int>>>(4, vector<vector<int>>(n, vector<int>(n)));
auto& dp = *dp_ptr;
for (int i = n-1; i >= 0; --i) {
for (int j = i; j < n; ++j) {
for (int k = 0; k < 4; ++k) {
char c = 'a' + k;
if (j == i) {
if (S[i] == c) dp[k][i][j] = 1;
else dp[k][i][j] = 0;
} else { // j > i
if (S[i] != c) dp[k][i][j] = dp[k][i+1][j];
else if (S[j] != c) dp[k][i][j] = dp[k][i][j-1];
else { // S[i] == S[j] == c
if (j == i+1) dp[k][i][j] = 2; // "aa" : {"a", "aa"}
else { // length is > 2
dp[k][i][j] = 2;
for (int m = 0; m < 4; ++m) { // count each one within subwindows [i+1][j-1]
dp[k][i][j] += dp[m][i+1][j-1];
dp[k][i][j] %= mod;
}
}
}
}
}
}
}
int ans = 0;
for (int k = 0; k < 4; ++k) {
ans += dp[k][0][n-1];
ans %= mod;
}
return ans;
}
};
class Solution {
public int countPalindromicSubsequences(String S) {
int n = S.length();
int mod = 1000000007;
int[][][] dp = new int[4][n][n];
for (int i = n-1; i >= 0; --i) {
for (int j = i; j < n; ++j) {
for (int k = 0; k < 4; ++k) {
char c = (char) ('a' + k);
if (j == i) {
if (S.charAt(i) == c) dp[k][i][j] = 1;
else dp[k][i][j] = 0;
} else { // j > i
if (S.charAt(i) != c) dp[k][i][j] = dp[k][i+1][j];
else if (S.charAt(j) != c) dp[k][i][j] = dp[k][i][j-1];
else { // S[i] == S[j] == c
if (j == i+1) dp[k][i][j] = 2; // "aa" : {"a", "aa"}
else { // length is > 2
dp[k][i][j] = 2;
for (int m = 0; m < 4; ++m) { // count each one within subwindows [i+1][j-1]
dp[k][i][j] += dp[m][i+1][j-1];
dp[k][i][j] %= mod;
}
}
}
}
}
}
}
int ans = 0;
for (int k = 0; k < 4; ++k) {
ans += dp[k][0][n-1];
ans %= mod;
}
return ans;
}
}
class Solution:
def countPalindromicSubsequences(self, S):
n = len(S)
mod = 1000000007
dp = [[[0 for _ in range(n)] for _ in range(n)] for _ in range(4)]
for i in range(n-1, -1, -1):
for j in range(i, n):
for k in range(4):
c = chr(ord('a') + k)
if j == i:
if S[i] == c: dp[k][i][j] = 1
else: dp[k][i][j] = 0
else: # j > i
if S[i] != c: dp[k][i][j] = dp[k][i+1][j]
elif S[j] != c: dp[k][i][j] = dp[k][i][j-1]
else: # S[i] == S[j] == c
if j == i+1: dp[k][i][j] = 2 # "aa" : {"a", "aa"}
else: # length is > 2
dp[k][i][j] = 2
for m in range(4): # count each one within subwindows [i+1][j-1]
dp[k][i][j] += dp[m][i+1][j-1]
dp[k][i][j] %= mod
ans = 0
for k in range(4):
ans += dp[k][0][n-1]
ans %= mod
return ans
示例演练
- 这是一个很难解决的问题且彻底理解它的解决办法也很有挑战性。理解上述方法的最好方法是演示一些简单的例子来帮助理解。
- 让我们先看看填写 dp 表时使用的策略,然后通过一个具体的示例来了解它是如何工作的。
复杂度分析
- 时间复杂度:$O(N^2)$ 其中 $N$ 是字符串 $S$ 的长度。填写 dp 表需要平方的时间
- 空间复杂度: $O(N^2)$ 其中 $N$ 是字符串 $S$ 的长度,相当于使用了常数个数的二维空间。
注意,我们上述分析中忽略了的常数因子 $4$。
总结
回过头来看,这个问题表明动态规划非常适合用来解决重叠的子问题。通过练习更多类似的问题,我们可以建立这种直觉。
方法二:动态规划(使用二维数组)
几乎每一个回文字符串都将采用这四种形式之一:a_a
、b_b
、c_c
或 d_d
,其中 _
表示零个或多个字符的回文字符串。(其他回文字符串只有 a
、b
、c
、d
和空字符串)
让我们试着数一数 a_a
形式的回文(其他形式是类似的)。我们应该取第一个和最后一个 a
,然后计算所有可以在其间形成的回文字符串。
算法:
- 定义
dp(i, j)
是字符串T = S[i], S[i+1], ..., S[j]
中的回文字符串个数(包括回文''
)。要在T
中计算a_a
形式的回文数,我们需要知道该字符串中'a'
第一次和最后一次出现的位置。定义next[i][0]
将是S[i:]
中'a'
的下一次出现的位置,next[i][1]
将是S[i:]
中'b'
下一次出现的位置,依此类推。 - 此外,我们还需要知道
T
中唯一字母的数目,才能计算出单个字母的回文数。我们可以用next
得到的信息来推断它:如果next[i][0]
在区间[i,j]
中,那么'a'
出现在T
中,以此类推。 - 由于许多状态
dp(i, j)
不需要计算,最直接的方法是自顶向下变化的动态规划。
class Solution(object):
def countPalindromicSubsequences(self, S):
N = len(S)
A = [ord(c) - ord('a') for c in S]
prv = [None] * N
nxt = [None] * N
last = [None] * 4
for i in xrange(N):
last[A[i]] = i
prv[i] = tuple(last)
last = [None] * 4
for i in xrange(N-1, -1, -1):
last[A[i]] = i
nxt[i] = tuple(last)
MOD = 10**9 + 7
memo = [[None] * N for _ in xrange(N)]
def dp(i, j):
if memo[i][j] is not None:
return memo[i][j]
ans = 1
if i <= j:
for x in xrange(4):
i0 = nxt[i][x]
j0 = prv[j][x]
if i <= i0 <= j:
ans += 1
if None < i0 < j0:
ans += dp(i0+1, j0-1)
ans %= MOD
memo[i][j] = ans
return ans
return dp(0, N-1) - 1
class Solution {
int[][] memo, prv, nxt;
byte[] A;
int MOD = 1_000_000_007;
public int countPalindromicSubsequences(String S) {
int N = S.length();
prv = new int[N][4];
nxt = new int[N][4];
memo = new int[N][N];
for (int[] row: prv) Arrays.fill(row, -1);
for (int[] row: nxt) Arrays.fill(row, -1);
A = new byte[N];
int ix = 0;
for (char c: S.toCharArray()) {
A[ix++] = (byte) (c - 'a');
}
int[] last = new int[4];
Arrays.fill(last, -1);
for (int i = 0; i < N; ++i) {
last[A[i]] = i;
for (int k = 0; k < 4; ++k)
prv[i][k] = last[k];
}
Arrays.fill(last, -1);
for (int i = N-1; i >= 0; --i) {
last[A[i]] = i;
for (int k = 0; k < 4; ++k)
nxt[i][k] = last[k];
}
return dp(0, N-1) - 1;
}
public int dp(int i, int j) {
if (memo[i][j] > 0) return memo[i][j];
int ans = 1;
if (i <= j) {
for (int k = 0; k < 4; ++k) {
int i0 = nxt[i][k];
int j0 = prv[j][k];
if (i <= i0 && i0 <= j) ans++;
if (-1 < i0 && i0 < j0) ans += dp(i0 + 1, j0 - 1);
if (ans >= MOD) ans -= MOD;
}
}
memo[i][j] = ans;
return ans;
}
}
复杂度分析
- 时间复杂度:$O(N^2)$ 其中 $N$ 是字符串 $S$ 的长度。我们对
prv
和nxt
的计算是在 $O(N)$ 时间内完成的,其中dp
有最多 $N^2$ 的状态,对每个状态的计算需要 $O(1)$ 的时间 - 空间复杂度:$O(N^2)$,
memo
使用的空间。
统计信息
通过次数 | 提交次数 | AC比率 |
---|---|---|
4111 | 8284 | 49.6% |
提交历史
提交时间 | 提交结果 | 执行时间 | 内存消耗 | 语言 |
---|
相似题目
题目 | 难度 |
---|---|
最长回文子序列 | 中等 |