一、题目描述
Given a string S, find the number of different non-empty palindromic subsequences in S, and return that number modulo 10^9 + 7
.
A subsequence of a string S is obtained by deleting 0 or more characters from S.
A sequence is palindromic if it is equal to the sequence reversed.
Two sequences A_1, A_2, ...
and B_1, B_2, ...
are different if there is some i
for which A_i != B_i
.
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 10^9 + 7.
Note:
- The length of
S
will be in the range[1, 1000]
. - Each character
S[i]
will be in the set{'a', 'b', 'c', 'd'}
.
二、题目分析
根据题意,我们需要计算出给定的由 a
、b
、c
、d
组成的字符串的回文子序列的数量。
采取动态规划的思路,考虑一个给定的字符串 S
,其回文子序列的数量为 F(S)
。由于是回文序列,从对称的角度,我们考虑在这个字符串的左右两端添加新的字母。
-
若添加的两个字母不同,即形如
aSb
的新字符串:
那么实际上相比于F(S)
,F(aSb)
新增的回文序列有,原字符串加入左端的字母a
后新增的回文子序列ΔF(aS)
,和原字符串加入右端的字母b
后新增的回文子序列ΔF(Sb)
,所以有F(aSb) = F(S) + ΔF(aS) + ΔF(Sb)
,而易知,ΔF(aS) = F(aS) - F(S)
,ΔF(Sb) = F(bS) - F(S)
。故最后有F(aSb) = F(aS) + F(Sb) - F(S)
。 -
若添加的两个字母相同,即形如
aSa
的新字符串(因为加入的是相同的字母,蕴含对称性,形成回文的可能更多):
首先能够直观地看到,F(aSa)
应是F(S)
的两倍左右,因为可以直接在原本S
的所有回文子序列的两端加入两个相同的字符来构成新的回文序列。此外,还要考虑到这两个新的字符本身的影响,以下分三种情况讨论。- 若添加的新字符
a
不在S
中,则在两倍的基础上,还多了两个回文子序列:a
和aa
。 - 若添加的新字符
a
在S
中出现过一次,则在两倍的基础上,多了一个回文子序列aa
(注意回文子序列aaa
已被算在两倍的数量中)。 - 若添加的新字符
a
在S
中出现过两次或两次以上,比如现在有如下结构的字符串aαaTaβa
,α
和β
是不包含a
的字符串(可能为空),那么计算两倍的时候,F(aTa)
会被重复计算,所以这时要减去这个值。
- 若添加的新字符
以上,对所有情况的讨论都完成了(当然,当添加的两个字符相同时,也能够按照不同的那个思路来计算)。这里递推式不是那么好写,故省略。
三、具体实现
具体实现中,用二维数组中的一个值 res[i][j]
表示位置 i
到位置 j
的子字符串的回文子序列的数量,实际任务就是填表,而从上面的分析可以看出,计算当前一个位置的值,需要表中它的左方和下方的值是已知的,故遍历时按照从最后一个字符开始倒序遍历,遍历到的字符作为字符串的起始字符,再从该字符串的下一个开始从左到右遍历,这样就能在计算当前值时,它所需要的作为前提的值都为已知。
另一个需要解决的问题是,如何知道某个字符是否在去头去尾后的中间的那个字符串中出现过,以及若出现过,其出现的次数。这里使用两个数组 first
和 last
来保存字符第一次出现和最后一次出现的位置(在初始的整个字符串的下标位置),而因为是按照子字符串长度的递增顺序来遍历(第二层遍历),所以在遍历时就能更新这些信息。这样,判断一个字符的第一次出现的位置和最后一次出现的位置就能知道所需要的信息了。
时间复杂度为 O ( N 2 ) O(N^2) O(N2) , N N N 为所给字符串的长度。
class Solution
{
public:
int countPalindromicSubsequences( string S )
{
int mod = 1e9 + 7, n = S.size();
vector<vector<int>> res( n, vector<int>( n, 0 ) );
for ( int i = 0; i < n; ++i )
res[i][i] = 1;
for ( int i = n - 1; i >= 0; --i ) {
vector<int> first( 4, -1 );
vector<int> last( 4, -1 );
for ( int j = i + 1; j < n; ++j ) {
int chaIdx = S[j] - 'a', prevLastIdx = 0;
if ( first[chaIdx] < 0 ) {
first[chaIdx] = j;
} else {
prevLastIdx = last[chaIdx];
last[chaIdx] = j;
}
if ( S[i] == S[j] ) {
res[i][j] = ( res[i + 1][j - 1] * 2 ) % mod;
if ( first[chaIdx] == j ) {
// the first occurrence of this character
res[i][j] += 2;
} else if ( first[chaIdx] < last[chaIdx] && prevLastIdx == -1 ) {
res[i][j] += 1;
} else {
// this character has appeared more than twice
res[i][j] -= res[first[chaIdx] + 1][prevLastIdx - 1];
if ( res[i][j] < 0 )
res[i][j] += mod;
}
} else {
res[i][j] = res[i][j - 1] + res[i + 1][j] - res[i + 1][j - 1];
if ( res[i][j] < 0 )
res[i][j] += mod;
}
res[i][j] %= mod;
}
}
return res[0][n - 1];
}
};