回文自动机及其可持久化

Posted 殇雪

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了回文自动机及其可持久化相关的知识,希望对你有一定的参考价值。

这玩意也叫回文树。主要解决回文串的问题。

回文串是一种十分特殊的字符串,拥有很多优美的性质。近年来,算法竞赛中有关回

文串的题目比较热门,但由于与回文串相关的算法比较贫乏,导致题目的解法比较单一。
回文树是一种新兴的数据结构,由Mikhail Rubinchik在2015年发表。(战斗民族发明的数据结构)。

这玩意在IOI2017中国国家候选队论文集里有,翁文涛dalao的《回文树及其应用》。

首先我们定义一些变量。
1.len[i]表示编号为i的节点表示的回文串的长度
2.net[i][c]表示编号为i的节点表示的回文串在两边添加字符c以后变成的回文串的编号(和Trie类似)。
3.fa[i]表示节点i失配以后跳转不等于自身的节点i表示的回文串的最长后缀回文串(类似后缀自动机的parent指针,就是找父亲)。
4.cnt[i]表示节点i表示的本质不同的串的个数(建树时求出的是极大回文串的个数,最后按照拓朴序跑一遍以后才是正确的)。
5.num[i]表示以节点i表示的最长回文串的最右端点为回文串结尾的回文串个数,也可以用来在建树过程中统计下标j的为结尾的回文串个数。
6.last指向新添加一个字母后所形成的最长回文串表示的节点。
7.S[i]表示第i次添加的字符(一开始设S[0] = -1(可以是任意一个在串S中不会出现的字符))。
8.p表示添加的节点个数。
9.n表示添加的字符个数。

那么我们就可以猛的搞一波事情了。

下面是一个字符串abbaabba的回文自动机的建立过程。 

我们可以发现,回文自动机是两颗树交错地生长在一起。

一颗的根为0,表示是偶数个回文串,一颗是1,表示是奇数数个回文串。

0指向1。(因为我们失配后最小的回文是单个字符本身。)
技术分享图片 
技术分享图片 
技术分享图片 
技术分享图片 
技术分享图片 
技术分享图片 
技术分享图片 
技术分享图片 
技术分享图片 
技术分享图片 
技术分享图片 
技术分享图片 
技术分享图片 
技术分享图片 
技术分享图片 
技术分享图片 
技术分享图片 

从这里我们可以发现令len[1]=-1的好处了,我们可以不用特判而加入单个字符的回文串。

那么我们就把朴素的PAM(回文自动机)讲完了,看一道例题:BZOJ 3676

#include<bits/stdc++.h>
#define N 300007
using namespace std;
int w,p,q,tot,now,last,cnt[N],fa[N],net[N][26],len[N],L;
int c[N],id[N];
long long ans;
char a[N];
void extend(int x){
    w=a[x]-a; p=last;
    while (a[x]^a[x-len[p]-1]) p=fa[p];
    if (net[p][w]) cnt[net[p][w]]++;
    else {
        q=++tot; len[q]=len[p]+2;now=p;
        do now=fa[now];while(a[x]^a[x-len[now]-1]);
        fa[q]=net[now][w];
        net[p][w]=q; cnt[q]=1;
    }  last=net[p][w];
}
int main () {
    freopen("a.in","r",stdin);
    scanf("%s",a+1);
    L=strlen(a+1);
    fa[0]=tot=1; len[1]=-1;
    for (int i=1;i<=L;i++) 
     extend(i);
    for (int i=2;i<=tot;i++) c[len[i]]++;
    for (int i=1;i<=L;i++) c[i]+=c[i-1];
    for (int i=2;i<=tot;i++) id[c[len[i]]--]=i;
    for (int i=tot-1;i;i--)  cnt[fa[id[i]]]+=cnt[id[i]];
    for (int i=2;i<=tot;i++) ans=max(ans,1ll*len[i]*cnt[i]);
    printf("%lld\n",ans);
}

我们考虑如何删除一个节点。

我们考虑可持久化

 我们发现直接对PAM可持久化的时间复杂度是不对的。我们发现PAM的时间复杂度是基于势能分析的,那么我们就不能直接持久化。因为我们如果反复插入删除复杂度高的操作,会退化成O(nq)的复杂度(q为操作数)。

我们需要一种不基于势能分析的插入算法之前的插入算法的瓶颈在于,每次插入一个字符c,都要沿着当前最长回文后缀t的f ail链往上找到第一个v使得v在s中的前驱(即v的前一个字符)为c。注意到除了v = t的情况,v的前驱总是在t内的,也就是说对于每个t,是要找到一个最长的t的回文后缀满足其前驱为c,这个回文后缀只与t相关而不与s相关。因此,对于每个回文树中的节点t,另外维护一个失配转移数组quick[c],存储t的最长的满足前驱为c的回文后缀。那么在插入时,我们只需首先检查当前最长回文后缀t的前缀是否为c,假如不是,那么合法的v直接就是t的quick[c]。接下来考虑怎么维护每个节点的quick。对于一个节点t,t的quick与f a的quick几乎没有什么差别,因为t的回文后缀只是在f a的回文后缀中再加入了f a而已。首先将把fa的quick 复制一遍作为t的quick,令c为f a在t中的前驱,用f ailt更新t的quick[c]即可。直接暴力操作每次插入的时空复杂度都是O(1)。可以将每个t的quick可持久化,一种做法是用可持久化线段树来可持久化数组。每次复制时只需要将版本复制,复杂度降为O(1)。插入的时空复杂度为O(log n )。最后可以得到一个不基于势能分析的单次插入时空复杂度为O(log ) 的算法。

 

以上是关于回文自动机及其可持久化的主要内容,如果未能解决你的问题,请参考以下文章

PAM / 回文自动机(回文树)略解

回文树总结

微信小程序代码片段

[算法模版]回文树

回文自动机入门题

蒟蒻Orion还要学的东西!