WOJ 1583 Palindrome(回文自动机)

来源:互联网 发布:js的继承 编辑:程序博客网 时间:2024/06/08 15:04

题目大意:给出一个字符串,求出从哪个位置分开,使得前半部分的本质不同的回文串个数恰是后半部分的两倍。如果有多个方案,把所有前半部分字符个数相乘起来后模1e9+7。


很神奇的一种数据结构,模板大部分参考了这篇博客:【回文自动机】ural2040 - huyuncong的专栏 - 博客频道 - CSDN.NET


个人觉得这个改进后的算法相较cf上的初始算法在性能上优越些。

回文自动机和AC自动机很相似,都拥有失配指针,并且利用next数组保存儿子信息。

然而因其回文的特性,next数组只会储存回文串的后半段(奇数长度的回文串会保存中间值)。

并且,结点的含义也变化了,不再像AC自动机一样,一个结点代表着一个字符,回文自动机的每个结点都代表着一个回文串。

所以回文自动机需要将字符串存储下来,结点总数就是串中回文串的总数。

 

另外,回文串支持在线操作,因为其本身就是在线的,即插入一个字符,就可以得到目前串的总回文串的个数。

 

首先要说明两个性质:

1.长度为n的串,其本质不同的回文串最多不会超过n个;

2.新加进来一个字符后,若以这个字符为结尾能构成新的回文串,那么这个回文串的最长长度不会超过之前串的最长后缀的长度再加2;并且,如果这个新的回文串长度是之前串的最长后缀的长度再加2,那么它将变为新的最长回文后缀。

 

性质1的证明:

首先两个本质不同的回文串必然不会拥有相同左右端点,假设两个回文串的长度一个是len1(len1 >=1),一个是len2(len2 >=1),那么为了包含这个两个回文串,则会出现三种情况:

a.两者不能构成相互包含的关系,则需要的最少空间是max(len1, len2) +1;

b.两者可以构成相互包含的关系,则需要的最少空间是max(len1, len2)>= min(len1, len2) + 1。

显然,为了包含n个本质不同的回文串,两两比较后,在考虑到回文串之间相互位置的情况下,则最少需要的空间是min(len1, len2,..., lenn) + (n - 1)。而min(len1, len2, ..., lenn) >= 1,所以为了包含n个本质不同的回文串最少需要的空间是n个字符。这样从侧面证明n个字符最多只能构成n个本质不同的回文串。

 

性质2的证明:

假设新加进来一个字符后,使得新的回文后缀的长度len2超过了原来的回文后缀长度len1再加2,那么说明新的回文后缀的去掉两个端点后,剩下的部分也是一个回文串,并且长度为len2 - 2 >len1。但去掉两个端点后,剩下部分就是之前没加新字符的后缀,这与最长回文后缀的定义相悖。

 

下面解释一下每个成员的含义。

 

首先先琢磨一下palin函数的意义:

已经知道l[x]是一个回文后缀的长度了,要看x+l[x]+y是不是回文串,只用检查x和y相不相同即可。


(为了不特判(在palin里面),我们规定字符串的其实下标为1,在第0位放上一个从未出现过的字符。)

 

next[i][k]结点i的以字母k为儿子的回文串的结点编号。如果不存在则为0。假定串bbb的编号为3,abbba的编号为6,那么next[3][0] = 6。

 

fail[i]表示当i失配时,应该去匹配的下一个次长回文后缀的编号。相当于最长回文后缀的除自己外的最长回文后缀。

 

比如abbabba的fail指向abba。

 

l[i]代表回文串i的长度。

 

last指针表示已经插入的串中,以最后一个字符为结尾的最长回文串的编号(最长回文后缀)。

 

ro:奇数根(恒定为1);re:偶数根(恒定为2)。ro的儿子都是单个字符的回文串(a、b、c等),re的儿子都是某个字符两个一起组成的回文串(aa、bb、cc等)。

 

last初始值指向ro;

len[ro] = -1(这样palin时比较的字符就是自己本身);

len[re] = 0(这样palin时 比较的字符是自己的前一个);

fail[ro]不会用到,因为自己跟自己一定会匹配成功;

fail[re] = ro,如果不能和前面一个字符构成aa型回文串,那只能自己和自己搭了。

 

接下来是Add操作。

 

Add操作可以一次完成添加字符,纪录子节点并为它构造fail指针,更新回文串总数,最后再更新last指针四个功能。若还想添加计数功能,再新添一个cnt数组,每到达一次某个回文串结点增加1即可,另外还可以添加纪录位置功能等。

 

Add操作是如何完成上述功能的呢?

 

首先已经知道last指向最长回文后缀x。我们无需关心这个回文串的具体内容是什么,只需要知道它的长度是多少就可了,因为Add函数的参数还包括了加进来字符c的位置k,通过查询len[x],看越过中间len[x]长的字符后,前第len[x]+1(位置应当是k-1-len[x])字符是不是和新加来的c相同,若相同,显然这是一个回文串,并且长度是len[k]+2。如果不能匹配,那么通过fail指针查下一个次长回文后缀。

 

假设回文后缀t的前一个字符和c匹配成功了,那么c+t+c就是一个回文串,如果c+t+c以前出现过(next[t][c]不为0),那么就是t的儿子next[t][c]。

 

如果不存在,回文树的结点增加,并纪录next[t][c]。接着为next[t][c]构造fail指针。如果t是ro,那么它的失配指针是re。后来者最不济也要和自己的前一个比较一下。

 

否则,和AC自动机的失配指针构造方法类似,找沿着t的失配指针,使得出现一个回文后缀w满足它的前一个和c匹配即可。那么c+t+c的fail指向next[w][c]。

 

并且next[w][c]是一定存在的。

 

为什么一定会存在呢?有三种情况:


1.    t为偶数回文后缀:

a. 找得到w,且w的长度小于或大于或等于t的一半,w的前一个字符c一定是关于t的对称轴对称的,那么c+w+c是一个已经出现过的回文串了;

b. 找不到和c匹配的回文后缀,那么沿着fail指针会走到ro,于是和自己匹配,"c"这个串是一定会存在的。


2.    t为奇数回文后缀:

a. 找得到w,且w加上前面的c的总长度小于或大于t的一半,w的前一个字符c一定是关于t的对称轴对称的,那么c+w+c是一个已经出现过的回文串了;

b. 找不到和c匹配的回文后缀,那么沿着fail指针会走到ro,于是和自己匹配,"c"这个串是一定会存在的

c. 唯一能和c匹配的字符正好在整个串的中间,那么会有这样的情况:

baaabaaab型,这个时候,baaabaaab的fail是指向baaab的,但很容易可以得出,分立对称轴两侧的左右两边(包括对称轴)是镜像相同的,所以baaab是一个已经存在过了的回文串;

aaabaaab型,这个时候,最长回文后缀是baaab,它的fail是指向b的,然后就很明晰了,b是必然存在的。

 

这样就是整个回文自动机的全部了,代码量不多。

 

然后是WOJ 1583的思路:

我们知道回文自动机可以在线的求出1~i的字符之间有多少个本质不同的回文串,想要求i+1~n之间的,只需要把字符串逆过来构造一次回文自动机就可以了。


推荐几个题目:

URAL 2040

BZOJ 2565

 

#include <algorithm>#include <iostream>#include <stdlib.h>#include <string.h>#include <stdio.h>using namespace std;#define MAXN 100100#define MAXK 26#define palin(x, id, c) (s[id - 1 - l[x]] - 'a' == c)#define MOD 1000000007#define LL long longchar str1[MAXN], str2[MAXN];int ans1[MAXN], ans2[MAXN];struct Trie{    int next[MAXN][MAXK], fail[MAXN], l[MAXN];    int sz, ro, re, last;    void Init(char *s)    {        sz = 0;        ro = ++sz, l[ro] = -1, fail[ro] = ro;        memset(next[sz], 0, sizeof(next[sz]));        re = ++sz, l[re] =  0, fail[re] = ro;        memset(next[sz], 0, sizeof(next[sz]));        last = ro;        s[0] = '$';    }    void Add(char *s, int c, int id)    {        while(!palin(last, id, c)) last = fail[last];        if (next[last][c]) last = next[last][c];        else        {            int x = last;            ++sz;            memset(next[sz], 0, sizeof(next[sz]));            next[x][c] = sz, l[sz] = l[x] + 2;            if (x == ro) fail[sz] = re;            else            {                x = fail[x];                while(!palin(x, id, c)) x = fail[x];                fail[sz] = next[x][c];            }            last = sz;        }    }}pt;int main(){//    freopen("J.in", "r", stdin);    int t; scanf("%d", &t);    getchar();    while(t--)    {//        scanf("%s", str1 + 1);        gets(str1 + 1);        int len = strlen(str1 + 1);        for(int i = 1; i <= len; i++) str2[len - i + 1] = str1[i];        pt.Init(str1);        for(int i = 1; i <= len; i++) pt.Add(str1, str1[i] - 'a', i), ans1[i] = pt.sz - 2;        pt.Init(str2);        for(int i = 1; i <= len; i++) pt.Add(str2, str2[i] - 'a', i), ans2[i] = pt.sz - 2;        LL ans = 1;        bool flag = true;        for(int i = 1; i < len; i++) if(ans1[i] == ans2[len - i] << 1)            ans *= i, ans %= MOD, flag = false;        if(flag) puts("0");        else printf("%lld\n", ans);    }    return 0;}


0 0