回文树(回文自动机),是解决一类回文串问题的强大数据结构,比 Manacher 扩展了很多功能。
这个数据结构比较新,由来自战斗民族的神犇MikhailRubinchik在2014年的Petrozavodsk夏令营提出。
这个数据结构代码量其实超级少。
Manacher
最好会至少一种自动机
回文树严格来讲是由两棵树构成的森林,再加上一堆后缀链(失配链)。其中一棵树代表长度为奇数的回文串,另一棵树代表长度为偶数的回文串。树上每个节点都代表一种与其它节点不同的回文串,所以每个点上都有诸如长度 len 和出现次数 cnt 之类的权值。
后缀链 fail 连向代表以这个点代表的回文串的最长回文后缀的点(有点长,自己断句)。
初始时,偶数树的根节点代表一个空串,长度为 0 ,后缀链连向奇数树的根节点。奇数树的根节点则比较神,代表一个被吞掉一个字符的串(是不是很鬼畜???),长度为 −1 (后面会讲为什么要这样),后缀链连向它自己。
插入操作过程中,我们需要记录当前插入的串的最长回文后缀 suf 。设原串为 s ,当我们要加入第 i 个字符时,我们就从 suf 这个点开始,往后缀链一直跳,直到到达一个点 x 使得 s[i−1−len[x]]=s[i] ,也就是该点代表的回文后缀两边字符相等,可以变成一个新的回文串(注意:当我们跳到奇数树根时, s[i−1−len[x]]=s[i−1−(−1)]=s[i] ,这样我们加入本质为单个字符的回文串了)。
我们找到 x 点后,先判断 x 是否存在两边字符为 s[i] 的儿子节点,如果有,就将儿子节点的 cnt 加一。否则新建节点, cnt 为 1 。那么新建点的后缀链怎么弄呢?我们可以对 fail[x] 做同样的过程(还是一直往 fail 跳,找合法节点),那么该点的 s[i] 儿子就是我们要找的 fail (当然存在找不到的情况,我们就将其 fail 设为空串即偶数树根,这个在实现时,我们可以将每个点不存在的儿子初始为偶数树根,这样就不用特判了)。
由于blog主特别懒,这里就不配图了,大家结合代码理解理解吧。
建完树,我们要重算一次每个回文串出现次数。过程很简单,就是倒序枚举节点,假设枚举到 x ,我们给 cnt[fail[x]] 加上 cnt[x] 即可。
然后剩下的就是结合题目乱搞了。
注:以下代码对应题目[APIO2014]回文串,也是算法的出处,推荐大家去做一做。
#include
#include
#include
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;i0;
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;iprintf("%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