回文自动机是接受一个字符串的所有回文子串的自动机。
回文自动机中每个点代表原串的一个回文子串。
维护两种指针:(trans),(fail)。
点 (x) 的 (trans[x][c]) 指针指向在这个点代表回文串两端同时加字符 (c) 后得到的回文串。
(fail) 指针指向这个点所代表的回文串的一个最长的回文串后(前)缀。
构造:增量法。
void insert(int l,int r,int c)
{
while(r-len[zr]-1<l||sz[r-len[zr]-1]!=c)
zr=fa[zr];
if(trs[zr][c])//存在
{
zr=trs[zr][c];
return;//直接返回
}
int w=trs[zr][c]=++sl/*新建节点*/,t=fa[zr];
while(t&&(trs[t][c]==0||r-len[t]-1<l||sz[r-len[t]-1]!=c))//寻找fail(注意)
t=fa[t];
if(trs[t][c])fa[w]=trs[t][c];
else fa[w]=2;//指向偶根
len[w]=len[zr]+2;zr=w;
}
细节:分为奇树,偶树,接受奇回文串,偶回文串。
设奇树根为1,偶树根为2。
将节点1的长度设为-1。节点2的长度设为0,fail设为1。
初始时的last设为1。
若没有fail指针,则fail指向2(偶根)。
int r0=2,r1=1;
fa[r0]=r1;zl=1;sl=2;
len[r0]=0;len[r1]=-1;
回文树的匹配类似AC自动机。
int w=1;//从1开始
for(int i=l;i<=r;i++)
{
while(trs[w][sz[i]]==0||i-len[w]-1<l||sz[i-len[w]-1]!=sz[i])//注意
w=fa[w];//走fail指针
w=trs[w][sz[i]];
qz[i]=w;
}
回文树具有性质:正反串的回文树是相同的。
因为每个节点都是回文,反转后不变,因此他们的trs,fail都是相同的。
这个性质,使得回文树可以支持在两端插入字符。
我们可以维护头尾两个 last 位置,然后在一个回文自动机中插入。
注意当整个串是一个回文串时需要把两个 last 都赋为这个回文串
if(len[zr]==r-l+1)zl=zr;
类似后缀自动机,回文自动机也可以定义right集合,求出回文串的出现次数。
将每次添加后的 last 设为红点。
在 fail 构成的树上,一个节点子树中红点个数就是它的出现次数。
这个也支持添加字符:
先离线读入所有添加字符操作,并记录每次的 last 。
对于每次操作,将last到根路径上都增加1即可。
在两端插入字符时,此法也是适用的。
定位子串可以倍增,具体方法类似后缀自动机:
先找到右端点代表的前缀,再按照长度倍增。
int getwzl(int l,int r)
{
int cd=r-l+1,u=qz[r];
if(len[u]<=cd)//注意此处
return u;
for(int i=17;i>=0;i--)
{
if(len[bz[i][u]]>cd)
u=bz[i][u];
}
return fa[u];
}
定位到的是这个子串的最长回文后(前)缀。