回文树简述
在大部分说法中,回文树与回文自动机指的是一个东西;
回文树是对一个字符串,基于自动机思想构建的处理回文问题的树形结构;
回文树是对着一个单串建立的;
于是他主要用于计数(回文子串种类及个数)
基本建立思路是建立其前缀的回文树,然后每加上一个字符,统计产生了什么回文;
回文树存在fail指针但一般不承接字符串匹配问题;
(回文树大概可以判定一个回文串是不是一个串的子串,但KMP之类的可以做得更好)
构建好的回文树,是这样的:
(好难看)
(点“偶”的fail指向点“奇”)
可看出:
存在两个树结构,分别记录奇数|偶数长度的回文;
每个点记录一种字符串(但是由于可以通过根到该节点的路径确定这个字符串是什么,于是并不需要真的在该节点记录/*写下*/这个信息)
每个节点连字符x边向一个子节点,表示在她的左右两边加x构成的回文是这个总字符串的子串(根节点相关部分单独定义);
每个节点连一条fail指针向其最长后缀回文;
几个性质
|s|表示s串的长度;
节点数不超过|s|:每个节点是互不相同的回文,每个回文都是某一个点的最长后缀回文,而每个点的最长后缀回文是唯一的;
转移数是O(|s|)级的:树有节点数-2个边(因为是两棵树),加上每个节点一个fail,于是是O(|s|)级的;
构造方法
回文树的基础插入算法:
建立s的回文树;
现在已建立了s的前缀x,然后考虑插入下一个字符;
每次插入最多只会把她的最长后缀回文贡献到树中;
证明:
c除最长后缀回文之外的后缀回文i是其最长后缀回文k的子串,则她关于k的中心有一个对称串j,由于k本身是对称的,于是j与i本质相同,由于j已经加入回文树中,于是i不必加入;
那么我们设x串之前的最长后缀回文是t,在加入c后,我们期望的c的最长后缀回文最长是ctc,
但t串前面不是字符c怎么办,
试试t的除自己外最长回文后缀的前一个字符是不是c,
若不行,再试试她的最长回文后缀。。。。
这样找的的回文如果还没存在与回文树中,则将其插入;
为了方便这一过程,我们维护每点一个指针fail,s当前的前缀x的最长回文后缀t,没有ctc,就跳fail,直到可以匹配;
建fail的过程与匹配过程类似;
然后维护一个last指向当前在的节点,以便插入新的回文;
通过势能分析法发现这个算法是每次插入均摊O(1),总共O(n)的;(反正我也不会)
例题
对读入的字符串,正反分别建回文树,在此同时分别记录以每个为起点|终点的最长前|后缀回文;
枚举断点,求最值即可;
1 #include<cstdio> 2 #include<cstring> 3 using namespace std; 4 struct dt{ 5 int ch[30],fail,len; 6 }; 7 struct Pld_T{ 8 int tot,last; 9 dt data[500500]; 10 int len[500500]; 11 char s[500500]; 12 void Init(){ 13 data[0].fail=1; 14 data[1].len=-1; 15 tot=1;last=0; 16 } 17 void insert(int x){ 18 int j; 19 while(s[x-data[last].len-1]!=s[x])last=data[last].fail; 20 if(!data[last].ch[s[x]-'a']){ 21 data[++tot].len=data[last].len+2; 22 j=data[last].fail; 23 while(s[x-data[j].len-1]!=s[x])j=data[j].fail; 24 data[tot].fail=data[j].ch[s[x]-'a']; 25 data[last].ch[s[x]-'a']=tot; 26 last=tot; 27 } 28 else 29 last=data[last].ch[s[x]-'a']; 30 len[x]=data[last].len; 31 } 32 }; 33 Pld_T pld_t1,pld_t2; 34 int main() 35 { 36 int i,j,k,len,ans=0; 37 pld_t1.Init();pld_t2.Init(); 38 scanf("%s",pld_t1.s+1); 39 len=strlen(pld_t1.s+1); 40 for(i=len,j=1;i>=1;i--,j++) 41 pld_t2.s[j]=pld_t1.s[i]; 42 pld_t2.s[0]=pld_t1.s[0]='#'; 43 pld_t2.s[len+1]=pld_t1.s[len+1]='