LeetCode 730. Count Different Palindromic Subsequences

一、题目描述

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'}.

 


 

二、题目分析

  根据题意,我们需要计算出给定的由 abcd 组成的字符串的回文子序列的数量。

  采取动态规划的思路,考虑一个给定的字符串 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 中,则在两倍的基础上,还多了两个回文子序列:aaa
    • 若添加的新字符 aS 中出现过一次,则在两倍的基础上,多了一个回文子序列 aa (注意回文子序列 aaa 已被算在两倍的数量中)
    • 若添加的新字符 aS 中出现过两次或两次以上,比如现在有如下结构的字符串 aαaTaβaαβ 是不包含 a 的字符串(可能为空),那么计算两倍的时候,F(aTa) 会被重复计算,所以这时要减去这个值。

  以上,对所有情况的讨论都完成了(当然,当添加的两个字符相同时,也能够按照不同的那个思路来计算)。这里递推式不是那么好写,故省略。

 


 

三、具体实现

  具体实现中,用二维数组中的一个值 res[i][j] 表示位置 i 到位置 j 的子字符串的回文子序列的数量,实际任务就是填表,而从上面的分析可以看出,计算当前一个位置的值,需要表中它的左方和下方的值是已知的,故遍历时按照从最后一个字符开始倒序遍历,遍历到的字符作为字符串的起始字符,再从该字符串的下一个开始从左到右遍历,这样就能在计算当前值时,它所需要的作为前提的值都为已知。

  另一个需要解决的问题是,如何知道某个字符是否在去头去尾后的中间的那个字符串中出现过,以及若出现过,其出现的次数。这里使用两个数组 firstlast 来保存字符第一次出现和最后一次出现的位置(在初始的整个字符串的下标位置),而因为是按照子字符串长度的递增顺序来遍历(第二层遍历),所以在遍历时就能更新这些信息。这样,判断一个字符的第一次出现的位置和最后一次出现的位置就能知道所需要的信息了。

  时间复杂度为 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];
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值