回文树/自动机
回文树/自动机
作用
一个能够维护所有本质不同回文子串的结构。
每个节点代表一个回文串。
构造
考虑增量构造,不妨假设已经构造出了 \(1\sim i-1\) 的结构,添加第 \(i\) 个字符时会产生什么变化。
不难发现,可能会额外产生以 \(i\) 结尾的回文串。
除去额外产生的回文串首尾字符后,仍是一个回文串,并且以 \(i-1\) 结尾。
考虑类似 KMP 的方式,设 \(fail_u\),代表 \(u\) 这个回文串的最长严格后缀回文子串。
如果我们动态维护了以 \(i-1\) 结尾的最长回文子串,那么我们可以通过 \(fail\) 指针遍历到所有以 \(i-1\) 结尾的回文子串。
注意到我们需要判断枚举的回文串左边一个字符和当前插入字符是否相同,已知我们知道枚举回文串的末尾位置,想要得知开头位置,我们可以额外维护每个回文串的长度 \(len_u\)。
此时有一个极其优美的性质,我们考虑最长的一个满足可以扩展后缀回文串。
首先,如果这个串之前出现过,更短的结构之前也一定出现过,我们只需更新以 \(i\) 结尾的最长回文子串即可。
否则,如果存在更短的满足可以扩展的后缀回文串,更短的串之前也一定出现过,此时画图容易证明,我们只需要新增添一个新的回文串即可,并且易得知这个串的 \(fail\) 指针指向更小的那个串,此时跳 \(fail\) 指针找到该串即可。
为了实现上诉的维护,应当额外维护 \(nex_{i,j}\) 表示回文子串 \(i\) 两端,额外添加字符 \(j\) 后形成的回文串对应的节点标号。
但还有一个小问题:假设现在往上跳到了空串,那么空串也可以扩展回文串。但空串向两侧扩展字符c,得到c还是cc?
有一个巧妙的方法,维护两个空节点代表一奇一偶,奇数的长度记为 \(-1\)。
发现完美符合我们的要求。
实现
点击查看代码
inline void init(){
last=1,len[1]=-1,len[0]=0;
fail[1]=fail[0]=1;
tot=1,last=1;
}
inline void Insert(int i,int c){
while(s[i]!=s[i-1-len[last]]) last=fail[last];
if(nex[last][c]){
last=nex[last][c];
}
else{
int s1=++tot;
int tp=fail[last];
while(s[i]!=s[i-1-len[tp]]) tp=fail[tp];
fail[s1]=nex[tp][c];
nex[last][c]=s1;
len[s1]=len[last]+2;
last=s1;
}
}
优美性质
一个回文串最多存在 \(n\) 个本质不同子串,因为我们每次新添加点数是 \(O(1)\) 的。
上述构造方法时间复杂度是 \(O(n)\) 的。
具体证明可以类似KMP的势能分析。

浙公网安备 33010602011771号