前言
最近B组有一道我不会的题,赶紧学习。
简介
我们知道,Manacher算法可以在
O(n)
O
(
n
)
的时间内求出以每个位置为中心的最长回文串(虽然我昨天还不知道Manacher算法是怎么做的)。但是如果要统计回文串的个数,Manacher就捉襟见肘了。于是,回文自动机闪亮登场!
回文自动机是解决回文串问题的一类数据结构。
这个数据结构比较新,是由来自战斗民族的MikhailRubinchik在2014年的Petrozavodsk夏令营提出。(http://codeforces.com/blog/entry/13959)
回文树其实是两棵树,分别是偶数长度的回文树和奇数长度的回文树,树中每一个节点代表一个回文串。
而回文自动机的方法与Manacher迥乎不同,反倒与KMP和AC自动机类似,所以如果你不会Manacher,也不必像我一样去学。
前置技能
KMP、AC自动机。
数据说明
- len[i]:节点i的回文串的长度
- next[i][c]:节点i的回文串在两边添加字符c以后变成的回文串的编号(和字典树的next指针类似)
- fail[i]:类似于AC自动机的fail指针,指向失配后需要跳转到的节点(即为i的最长回文后缀且不为i)
- cnt[i]:节点i表示的回文串在S中出现的次数(建树时求出的不是完全的,count()加上子节点以后才是正确的)
- num[i]:以节点i回文串的末尾字符结尾的但不包含本条路径上的回文串的数目。(也就是fail指针路径的深度)
- last:指向以字符串中上一个位置结尾的回文串
- cur: 指向由next构成的树中当前回文串的父亲节点(即当前回文串是cur左右两边各拓展一个字符得来)
- S[i]:第i次添加的字符
- p:添加的节点个数
- n:添加的字符个数
分析
假设现在我们有串S=’abbaabba’。
先建两个树根,偶数长度的根为0,奇数长度的根为1。注意,我们将len[0]设为0,但将len[1]设为-1。将p、n、last均初始化为0。将S[0]设为-1,这是放一个字符集中没有的字符,减少特判。然后,我们将fail[0]指向1。
举个例子,若有串S=’abbaabba’。
首先我们添加第一个字符’a’,S[++ n] = ‘a’,然后将cur赋为get_fail(last)。其中的get_fail函数就是让找到第一个使得S[n-len[last]-1]==S[n]的last。注意,此处的n不为get_fail中的参数,依然为添加的字符个数。这样做的话,我们就可以通过fail构成的失配链找到last的所有回文后缀(包括它自己),然后从长到短依次判断此后缀的前一位是否等于S[n],等于则表明可以构成一个回文串。
判断此时next[cur][‘a’]是否已经有后继,如果next[cur][‘a’]没有后继,我们就进行如下的步骤:
新建节点(节点数p++,且之后p=3),并让now等于新节点的编号(now=2),则len[now]=len[cur]+2(每一个回文串的长度总是在其最长子回文串的基础上在两边加上两个相同的字符构成的,所以是+2,同时体现出我们让len[1]=-1的优势,一个字符自成一个奇回文串时回文串的长度为(-1)+2=1)。
然后我们让fail[now]=next[get_fail ( fail[cur] )][‘a’],即得到fail[now](此时为fail[2] = 0)。计算get_fail(fail[cur])是为了求出在cur的所有回文后缀中(不包括它自己,因为和AC自动机一样,fail[now]不能指向now),满足前一位等于S[n]的后缀,我们即可用它来往两边拓展一格,即为now的最长回文后缀(不包括它自己)。然后next[cur][‘a’] = now。
当上面步骤完成后我们让last = next[cur][‘a’](不管next[cur][‘a’]是否有后继),然后cnt[last] ++。
如上述方法插完所有字符后,我们将节点x在fail指针树中将自己的cnt累加给父亲,从叶子开始倒着加,最后就能得到串S中出现的每一个本质不同(两个串长度不同或者长度相同且至少有一个字符不同便是本质不同可以使用burnside引理)回文串的个数。
构造回文树需要的空间复杂度为
O(N∗字符集大小)
O
(
N
∗
字
符
集
大
小
)
,时间复杂度为O
(N∗log(字符集大小)
(
N
∗
l
o
g
(
字
符
集
大
小
)
),这个时间复杂度比较神奇。如果空间需求太大,可以改成邻接表的形式存储,不过相应的要牺牲一些时间。
功能
- 求串S前缀0~i内本质不同回文串的个数
- 求串S内每一个本质不同回文串出现的次数
- 求串S内回文串的个数(其实就是1和2结合起来)
- 求以下标i结尾的回文串的个数
例题
【JZOJ3654】【APIO2014】回文串(palindrome)
Problem
考虑一个只包含小写拉丁字母的符串s。我们定义s的一个子串t的“出现值”为 t在s中的出现次数乘以t的长度。 请你求出s的所有回文子串中的最大出现值。
Hint
Solution
一丝不挂的裸题。我们只需求出每个回文串的长度及其出现的次数即可。可以参考使用《分析》中倒数第二段的统计个数方法。
Code
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define N 300010//绝对要多开一点,因为回文树会多开两个节点,我就被这坑过一次
#define ll long long
#define fo(i,a,b) for(i=a;i<=b;i++)
#define fd(i,a,b) for(i=a;i>=b;i--)
int i,nn;
ll ans;
char s[N];
struct Palindrome_Automaton//回文自动机
{
int i,len[N],next[N][26],fail[N],cnt[N],last,cur,S[N],p,n;
int newnode(int l)//新建节点
{
fo(i,0,25)next[p][i]=0;//新建的节点为p,先消除它的子节点
cnt[p]=0;
len[p]=l;
return p++;//勿打成++p,因为此节点为p,我们应返回p
}
inline void init()//初始化
{
p=n=last=0;
newnode(0);
newnode(-1);
S[0]=-1;
fail[0]=1;
}
int get_fail(int x)
{
while(S[n-len[x]-1]!=S[n])x=fail[x];
return x;
}
inline void add(int c,int pos)//插字符
{
c-='a';
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];
next[cur][c]=now;
}
last=next[cur][c];
cnt[last]++;
}
void count()//统计本质相同的回文串的出现次数
{
fd(i,p-1,0)//逆序累加,保证每个点都会比它的父亲节点先算完,于是父亲节点能加到所有子孙
cnt[fail[i]]+=cnt[i];
}
}run;
int main()
{
freopen("palindrome.in","r",stdin);
freopen("palindrome.out","w",stdout);
scanf("%s",&s);
run.init();
nn=strlen(s)-1;//千万要先把这个记录下来,因为求长度的时间复杂度是O(n)——直接扫一遍,碰到结束符才停止,我一开始把它直接塞进下方循环的nn里,就T了一遍
fo(i,0,nn)run.add(s[i],i);
run.count();
fo(i,2,run.p-1)ans=max(ans,(ll)run.len[i]*run.cnt[i]);
printf("%lld",ans);
}