CF 932G - Palindrome Partition 回文树 技巧 DP

本文介绍了一种解决特定回文串划分问题的高效算法。该问题要求将输入字符串以特定方式划分为多个子串,使得所有子串均为偶长且构成回文结构。通过巧妙地重新组织输入字符串并利用回文树和DP技术,该算法能在nlogn的时间复杂度内求解问题。

题意:

给你一个字符串,问有多少种划分,使得如果划分后有k个部分,则sub[i] = sub[k - i + 1],s[i]是第i个部分代表的串,k必须是偶数。

题解:

普通的回文串是s[1]对应s[n],s[2]对应s[n - 1].....这里有不同,是s[1] 对应 s[n - j], s[2]对应s[n - j + 1],注意到现在对应关系是双方下标都在递增,而普通回文串是第一个下标增加 ,第二个下标减小。到这里我们可以想到把字符串倒过来一下,对于这题我们应该把字符串变成s[1]s[n]s[2]s[n - 1]s[3]s[n - 2]......然后问题就被转换了,现在相当于求当前串划分成一些偶长度的串,使得每个串都是回文串的方案数。

然后就可以用回文树和DP来做了。

可是现在还是有问题。假如沿着fail链DP,是n ^ 2的,为什么呢?

如果串是aaaaaaaa.....那么每个fail链长度都是O(n)的。

所以我们就要用到下面这些性质了:

    1.一个回文串S的后缀T如果是回文串等价于T是S的border 
    2.对于若干个回文后缀,如果长度都大于len/2,那么它们构成了一个等差数列
    3.将一个串S的所有border按长度从小到大排序后,能形成log个等差数列

第三条可以通过第二条推导过来,为什么呢?当前长度len有一个等差数列, len / 2 又有一个, len / 2 / 2 又有.....那么就是log个等差数列。

我们用f[i]记录[1...i]时的方案数,g[i]记录以回文串i为最大长度的等差数列的当前f值和。

更新g[p]的时候,假如当前这个等差数列是a1, a2, a3 。长度a1 > a2 > a3, 根据回文串的对称性,实际上f[i - a1], f[i - a2]都在g[fail[p]]里面算过了,只要加上f[i - a3]即可。

然后如果i是偶数f[i]才加上g[p]。

复杂度是nlogn。

代码:

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <queue>
#include <bitset>
#include <map>
#include <vector>
#include <stack>
#include <set>
#include <unordered_map>
#include <unordered_set>
#include <cmath>
#ifdef LOCAL
#define debug(x) cout<<#x<<" = "<<(x)<<endl;
#else
#define debug(x) 1;
#endif

#define chmax(x,y) x=max(x,y)
#define chmin(x,y) x=min(x,y)
#define lson id<<1,l,mid
#define rson id<<1|1,mid+1,r
#define lowbit(x) x&-x
#define mp make_pair
#define pb push_back
#define fir first
#define sec second
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<ll, int> pii;

const ll MOD = 1e9 + 7;
const double eps = 1e-10;
const int INF = 0x3f3f3f3f;
const ll INFLL = 0x3f3f3f3f3f3f3f3f;
const int MAXN = 1e6 + 5;


char s[MAXN];
char str[MAXN];
ll f[MAXN];
ll g[MAXN];


const int N = 26 ;
/*
	1.一个回文串S的后缀T如果是回文串等价于T是S的border
	2.对于若干个回文后缀,如果长度都大于len/2,那么它们构成了一个等差数列
	3.将一个串S的所有border按长度从小到大排序后,能形成log个等差数列
*/

struct Palindromic_Tree {
    int next[MAXN][N] ;//next指针,next指针和字典树类似,指向的串为当前串两端加上同一个字符构成
    int fail[MAXN] ;//fail指针,失配后跳转到fail指针指向的节点
    int cnt[MAXN] ; //表示节点i表示的本质不同的串的个数(建树时求出的不是完全的,最后count()函数跑一遍以后才是正确的)
    int num[MAXN] ; //表示以节点i表示的最长回文串的最右端点为回文串结尾的回文串个数
    int len[MAXN] ;//len[i]表示节点i表示的回文串的长度(一个节点表示一个回文串)
    int S[MAXN] ;//存放添加的字符
    int last ;//指向新添加一个字母后所形成的最长回文串表示的节点。
    int n ;//表示添加的字符个数。
    int p ;//表示添加的节点个数。
    int dif[MAXN];
    int id[MAXN];
    int go[MAXN];

    int newnode ( int l ) {//新建节点
        for ( int i = 0 ; i < N ; ++ i ) next[p][i] = 0 ;
        cnt[p] = 0 ;
        num[p] = 0 ;
        len[p] = l ;
        return p ++ ;
    }

    void init () {//初始化
        p = 0 ;
        newnode (  0 ) ;
        newnode ( -1 ) ;
        last = 0 ;
        n = 0 ;
        S[n] = -1 ;//开头放一个字符集中没有的字符,减少特判
        fail[0] = 1 ;
    }

    int get_fail ( int x ) {//和KMP一样,失配后找一个尽量最长的
        while ( S[n - len[x] - 1] != S[n] ) x = fail[x] ;
        return x ;
    }

    void add ( int c ) {
        S[++ n] = c ;
        int cur = get_fail ( last ) ;//通过上一个回文串找这个回文串的匹配位置
        if ( !next[cur][c] ) {//如果这个回文串没有出现过,说明出现了一个新的本质不同的回文串
            int now = newnode ( len[cur] + 2 ) ;//新建节点
            fail[now] = next[get_fail ( fail[cur] )][c] ;//和AC自动机一样建立fail指针,以便失配后跳转
            next[cur][c] = now ;
            num[now] = num[fail[now]] + 1 ;
            dif[now] = len[now] - len[fail[now]];
            go[now] = dif[now] == dif[fail[now]] ? go[fail[now]] : fail[now];
        }
        id[n] = last = next[cur][c] ;
        cnt[last] ++ ;
    }

    //abbacddc
    void solve() {
        for (int i = 1; i <= n; i++) {
            int idx = id[i];
            int cnt = 0;
            for (int j = idx; j; j = go[j]) {
                cnt++;
                g[j] = f[i - len[go[j]] - dif[j]];
                if (dif[j] == dif[fail[j]]) g[j] = (g[j] + g[fail[j]]) % MOD;
                if(i % 2 == 0) f[i] = (f[i] + g[j]) % MOD;
            }
        }
    }
} PAM;


int main() {
#ifdef LOCAL
    freopen ("input.txt", "r", stdin);
#endif
    PAM.init();
    scanf ("%s", s + 1);
    int n = strlen (s + 1);
    if (n & 1) return 0 * puts ("0");
    int cnt = 0;
    for (int i = 1; i <= n / 2; i++) {
        str[++cnt] = s[i];
        str[++cnt] = s[n - i + 1];
    }
    for (int i = 1; i <= n; i++) PAM.add (str[i] - 'a');
    f[0] = 1;
    PAM.solve();
    printf("%lld\n", f[n]);
    return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值