回文树(Palindrome Tree)/回文自动机(Palindrome Automaton)学习小记

来源:互联网 发布:seo常用的工具 编辑:程序博客网 时间:2024/05/18 07:22

简介

回文树(回文自动机),是解决一类回文串问题的强大数据结构,比Manacher扩展了很多功能。
这个数据结构比较新,由来自战斗民族的神犇MikhailRubinchik在2014年的Petrozavodsk夏令营提出。
这个数据结构代码量其实超级少。

必备技能

Manacher
最好会至少一种自动机


分析

回文树严格来讲是由两棵树构成的森林,再加上一堆后缀链(失配链)。其中一棵树代表长度为奇数的回文串,另一棵树代表长度为偶数的回文串。树上每个节点都代表一种与其它节点不同的回文串,所以每个点上都有诸如长度len和出现次数cnt之类的权值。
后缀链fail连向代表以这个点代表的回文串的最长回文后缀的点(有点长,自己断句)。
初始时,偶数树的根节点代表一个空串,长度为0,后缀链连向奇数树的根节点。奇数树的根节点则比较神,代表一个被吞掉一个字符的串(是不是很鬼畜???),长度为1(后面会讲为什么要这样),后缀链连向它自己。
插入操作过程中,我们需要记录当前插入的串的最长回文后缀suf。设原串为s,当我们要加入第i个字符时,我们就从suf这个点开始,往后缀链一直跳,直到到达一个点x使得s[i1len[x]]=s[i],也就是该点代表的回文后缀两边字符相等,可以变成一个新的回文串(注意:当我们跳到奇数树根时,s[i1len[x]]=s[i1(1)]=s[i],这样我们加入本质为单个字符的回文串了)。
我们找到x点后,先判断x是否存在两边字符为s[i]的儿子节点,如果有,就将儿子节点的cnt加一。否则新建节点,cnt1。那么新建点的后缀链怎么弄呢?我们可以对fail[x]做同样的过程(还是一直往fail跳,找合法节点),那么该点的s[i]儿子就是我们要找的fail(当然存在找不到的情况,我们就将其fail设为空串即偶数树根,这个在实现时,我们可以将每个点不存在的儿子初始为偶数树根,这样就不用特判了)。

由于blog主特别懒,这里就不配图了,大家结合代码理解理解吧。

建完树,我们要重算一次每个回文串出现次数。过程很简单,就是倒序枚举节点,假设枚举到x,我们给cnt[fail[x]]加上cnt[x]即可。
然后剩下的就是结合题目乱搞了。


代码实现

注:以下代码对应题目[APIO2014]回文串,也是算法的出处,推荐大家去做一做。

#include <iostream>#include <cstring>#include <cstdio>using namespace std;const int N=300005;const int C=26;long long ans;char str[N];int n;struct Palindrome_Tree{    int next[N][C],cnt[N],fail[N],len[N];    int tot,suf;    int newnode()    {        for (int i=0;i<C;i++)            next[tot][i]=0;        cnt[tot]=0,fail[tot]=0;        return tot++;    }    void init()    {        tot=0;        int p;        len[p=newnode()]=0;        p=fail[p]=newnode();        len[p]=-1;        fail[p]=p;        suf=0;    }    int getfail(int x,int l)    {        while (str[l-1-len[x]]!=str[l])            x=fail[x];        return x;    }    int insert(int x)    {        int c=str[x]-'a';        int p=getfail(suf,x);        if (!next[p][c])        {            int q=newnode();            len[q]=len[p]+2;            fail[q]=next[getfail(fail[p],x)][c];            next[p][c]=q;        }        p=next[p][c];        cnt[p]++;        suf=p;        return suf;    }    void calc()    {        for (int i=tot-1;i>=0;i--)        {            ans=max(ans,1ll*cnt[i]*len[i]);            cnt[fail[i]]+=cnt[i];        }    }}t;int main(){    freopen("palindrome.in","r",stdin);    freopen("palindrome.out","w",stdout);    scanf("%s",str);    n=strlen(str);    t.init();    for (int i=0;i<n;i++)        t.insert(i);    t.calc();    printf("%lld\n",ans);    fclose(stdin);    fclose(stdout);}

复杂度

首先有一条定理

长度为n的字符串本质不同的回文子串数目最多为n。

证明有很多,可以通过Manacher算法过程证明,也可以分析字符串性质,在这里不再讨论,参考资料的最后一项讲了。
然后空间复杂度显然为O(n|Σ|)Σ为字符集)或O(n),取决于你怎么存树。
时间复杂度我是这样分析的,我们考虑后缀链的长度,显然最多为n,每次插入的过程就相当于给后缀链长度减去若干之后加上1(包括计算fail的过程在内),长度最多加n次,显然我们最多也只能减n次。所以时间复杂度为O(n)


参考资料

国外博客:http://adilet.org/blog/25-09-14/
Codeforces上的介绍:http://codeforces.com/blog/entry/13959
一个不懒的人(然而他配的图也是copy原论文的)的博客:http://blog.csdn.net/u013368721/article/details/42100363
PDF的论文(不是原论文,作者Victor Wonder,写的很详细,UOJ上有链接):PalindromicTree.pdf

0 0