回文自动机小记
command_block
2020-07-31 15:07:15
# 1.基本元素与构造
- **回文串**
约定 $s_R$ 表示将字符串 $s$ 翻转形成的字符串。
满足 $s=s_R$ 的字符串 $s$ 称之为回文串。
本文编写历程时间跨度较大,由于前后习惯不同,可能混用大小写字母表示字符串。
- ## 回文自动机的基本元素
每个节点表示原串的一个回文子串,不同的节点表示的回文子串本质不同。
转移边表示给当前串左右同时加上某个字符。若有 $u\rightarrow v$ ,则称 $u$ 为 $v$ 的前驱, $v$ 为 $u$ 的后继(之一)。
$fail$ 边指向自己的最长回文后缀,不难发现 $fail$ 形成一棵有根内向树。
称从结尾在原串末尾的最长回文子串开始向上跳 $fail$ 得到的链为**终止链**,其在原串中对应着这样的一系列回文子串。
![](https://cdn.luogu.com.cn/upload/image_hosting/upq9smwa.png)
即原串的所有回文后缀。
- ## 回文自动机的经典构造方法
增量法构造,每次在原串末尾加入一个字符,不妨设为 $\rm x$。
考虑加入 $\rm x$ 后的新回文后缀,如下图 :
![](https://cdn.luogu.com.cn/upload/image_hosting/1sb2zdha.png)
- **性质①** 每次最多只会加入一个新的本质不同的回文串,且是所有回后缀中最长的。
如图,红色串为最长回文后缀,绿色串为某个较短的回文后缀。
![](https://cdn.luogu.com.cn/upload/image_hosting/oqr9euk4.png)
则根据回文的性质,绿色串必然在之前已经出现过了。
**推论** :本质不同的回文子串(回文自动机的点数)不超过 $|S|$。
现在,问题就是如何找到这个最长的新回文后缀。
对于每个新回文后缀,若在其两侧去掉一个 $\rm x$ ,就是一个旧的回文后缀。
考虑新出现的回文串(红色),则绿色部分(前驱)为旧终止链中的某个串。
![](https://cdn.luogu.com.cn/upload/image_hosting/3upv8peg.png)
那么,可以暴力跳旧终止链,并检查对应回文串左侧是否恰好有一个 $\rm x$。
由于终止链上长度是由下而上递减的,一旦找到一个符合要求的点 $v$ ,即找到了最长的新回文后缀。
若这个 $v$ 已经有 $\rm x$ 出边,则表示该最长回文后缀已经在前面出现过,找出 $d:v\xrightarrow{\rm x}d$ ,本次终止链末尾即为 $d$。
否则,最长回文后缀是全新的,新建点 $u$ 装这个后缀,连一条 $v\xrightarrow{\rm x}u$ ,且 $u.len=v.len+2$。
还有一个问题是,该串的 $fail$ 指向的新次长回文后缀如何寻找。
可以顺着终止链继续找到旧的回文后缀 $v'$ 满足左边有一个 $\rm x$。
根据 **性质①** 的证明,新次长回文后缀在前面出现过,所以此时 $v'$ 必有 $\xrightarrow{\rm x}$ 出边。
找出 $u':v'\xrightarrow{\rm x}u'$ ,则 $u.fail=u'$。
- **复杂度证明**
每次加入字符时,只会新增一个点和一个转移,状态总量显然是 $O(n)$ 的。
每次暴力跳终止链的复杂度令人怀疑,其实也是 $O(n)$ 的。
设当前终止链长度为 $k$ ,若跳过 $d$ 步才找到 $u.fail$ ,则新的由 $u$ 开始的终止链长度为 $k-d+1$。
终止链长度的消长相当于蜗牛爬杆,复杂度是正确的均摊 $O(n)$ 。
- **实现细节**
奇回文串的构造并不完全遵守“左右同时加上某个字符”的规则,中心字符是单独出现的。
所以,需要建立奇偶两个根,规定奇根转移得出的都是奇回文串,偶根转移得出的都是偶回文串。
对奇根采取特殊规则,走转移只会增加一个字符,即为某个奇回文串的重心字符。
为了兼容,可以把奇根的 $len$ 置为 $-1$ (这样可以看作加入两个重叠的字符)。
偶根的 $fail$ 为奇根 (奇根的限制比偶根少,若没有偶回文串,尝试一下奇回文串的最低要求 : 单个字符) ,奇根的 $fail$ 指向自己。
加入字符 $\rm x$ 的构造过程中,如果发现串没有新次短回文后缀,则意味着前面的串中根本没有字符 $\rm x$,此时将最长回文后缀(只有一个字符 $\rm x$) 的 $fail$ 置为偶根。
可能父亲和出边到自己的点是同一个点,要注意赋值顺序问题。
此外,为了避免越界,让字符串从 $1$ 开始标号,并把第 $0$ 个字符标记为 $-1$。
```cpp
struct Node
{int t[26],f,len;}a[MaxN];
int tn,las;
char str[MaxN];
void ins(int k,int c)
{
int p=las; //las是旧终止链的末端
while(str[k-a[p].len-1]!=c)p=a[p].f; //找到新最长回文后缀的前驱
if (!a[p].t[c]){ //若新最长回文后缀是全新的
int np=las=++tn,v; //新建点 np 装新最长回文后缀,其为新终止链的末端
for(v=a[p].f;str[k-a[v].len-1]!=c;v=a[v].f);//找到新次长回文后缀的前驱
if (!a[v].t[c])a[np].f=2; //找不到,则 fail 为偶根 (空回文串)
else a[np].f=a[v].t[c]; //否则由前驱找到新次长回文后缀
a[a[p].t[c]=np].len=a[p].len+2;
}else las=a[p].t[c]; //若新最长回文后缀出现过,简单地更改终止链末端
}
void Init()
{a[1].len=-1;a[1].f=a[2].f=1;tn=las=2;}
//奇根为1 偶根为2
```
- ## 经典应用
- **本质不同回文串数**
点数$-2$ (除去两个根)
- **各个回文子串的出现次数**
构造过程中,每加入一个字符时,相当于把整条终止链的出现次数 $+1$ 。
那么我们把每次的终止链末尾权值 $+1$ ,最后在 $fail$ 树上求和即可。
实际操作中,由于编号大的点一定不会是编号小的点的父亲,不需要建立树,直接按照编号求和也可。
- [P3649 [APIO2014]回文串](https://www.luogu.com.cn/problem/P3649)
求出出现次数之后,乘上 $len$ 取 $\max$ 即可。
[评测记录](https://www.luogu.com.cn/record/36494325)
- [P5496 【模板】回文自动机(PAM)](https://www.luogu.com.cn/problem/P5496)
本题相当于在线地询问终止链长度,记录 $dep$ 并递推。
[评测记录](https://www.luogu.com.cn/record/36494752)
- [P4287 [SHOI2011]双倍回文](https://www.luogu.com.cn/problem/P4287)
注意到双倍回文串也是一个回文串,此题可以转化成对于每个回文串,判定是否能拆成两个相同的偶回文串。
可以从该点跳 $fail$ 找到自己的所有回文后缀。若有长度恰为 $len/2$ 的,则符合要求。
暴力跳复杂度显然不对劲。
建立 $fail$ 树,$dfs$ 时开一个桶记录到根的链中各个点的 $len$ ,对于每个点只需要查看桶中为 $len/2$ 的位置。
[评测记录](https://www.luogu.com.cn/record/36496018)
- [P4555 [国家集训队]最长双回文串](https://www.luogu.com.cn/problem/P4555)
对于每个位置,记录在此结束和开始的最长回文串长度,然后拼接即可。
可以正反分别构造一次回文自动机来求出。
[评测记录](https://www.luogu.com.cn/record/36506828)
- [CF17E Palisection](https://www.luogu.com.cn/problem/CF17E)
可以转化为求不相交的回文串对子数。
枚举靠前的串的结尾位置 $i$ ,则靠后的串只需要在 $i$ 以后开始即可。
记录 $L[i]$ 为在 $i$ 开始的回文串个数, $R[i]$ 为结束。(即为终止链长度)
可以正反构造两次回文自动机来求出。
$\sum\limits_{i=1}^{|S|}R[i]\sum\limits_{j=i+1}^{|S|}L[j]$ 即为答案。
[评测记录](https://www.luogu.com.cn/record/36542284)
- [P5555 秩序魔咒](https://www.luogu.com.cn/problem/P5555)
其实就是双串最长公共回文子串。
对两个串分别建立回文自动机,由于状态量很少,可以选取两个自动机相同的转移进行搜索,即可得到所有的公共回文子串。
[评测记录](https://www.luogu.com.cn/record/36556331)
- [P5685 [JSOI2013]快乐的 JYY](https://www.luogu.com.cn/problem/P5685)
题意就是求两个字符串的相等回文子串对子数。
类似上一题,对同样的转移搜索,将对应节点的出现次数相乘求和。
[评测记录](https://www.luogu.com.cn/record/36557597)
另一种做法是,先建立 $A$ 的 $\rm PAM$ ,然后用 $B$ 在上面跑匹配。
逐个加入 $B$ 中的字符,维护在 $A$ 中能匹配到的最长回文后缀。
注意,匹配的时候是双向扩展,除了检查自动机的出边外,还要检查 $B$ 串对应的两个字符是否相等。
若匹配到 $u$ 点,则可以视为包含到了所有 $u$ 的祖先,对次数的贡献就是这一条链的 $siz$ 和。
那么预先向下求和即可。
[评测记录](https://www.luogu.com.cn/record/36557978)
- [CF835D Palindromic characteristics](https://www.luogu.com.cn/problem/CF835D) 加强版
原题 $|S|\leq 5000$ ,利用 $\rm PAM$ 可以做到 $|S|\leq 5\times 10^6$。
记 $\rm PAM$ 中串 $u$ 长度不超过 $len/2$ 的**最长**回文后缀为 $u.half$。
在构造回文自动机的同时,若新产生的点为 $np$ ,且找到 $p\xrightarrow{\rm x}np$ ,那么先找到 $p.half$ ,然后向祖先跳直到能产生对应回文后缀为止(注意不能直接检查有无出边,就算有也可能只是前面的)。
此时出边指向的那个点就是 $u.half$。复杂度证明类似前面对终止链长度变化的分析。
记 $f[u]$ 表示回文串 $u$ 的阶数,有如下 $\rm DP$ :
$\begin{cases}f[u]=f[u.half]+1&(u.half.len=u.len/2)\\f[u]=1&(u.half.len<u.len/2)\end{cases}$
[评测记录](https://www.luogu.com.cn/record/45270857)
- [P4762 [CERC2014]Virus synthesis](https://www.luogu.com.cn/problem/P4762)
能够发现,答案肯定是在某次 $2$ 操作之后暴力加字符得来的。
我们建立原串的回文自动机,然后分别求出每个(偶)回文子串 $S$ 的构造代价 $S$ ,原串的构造代价即为 $n+\min F[S]-len[S]$。
考虑如何构造一个回文串,不难发现,最优策略的最后一步操作一定是 $2$。
看看上一次的 $1$ 是怎么用的。
如果用在外面,则可以视为在两侧同时加了一个字符,则可以按照从后缀自动机的转移边来 $DP$。
转移为 $f[v]=\min(f[v],f[u]+1)\ (u\rightarrow v)$
如果用在里面,则必然先造好一个后缀,这里只需要考虑回文后缀。
设 $S$ 为目标, $T1,T2$ 为两个能够转移的回文后缀,若 $|T1|>|T2|$ 则 $T1$ 不劣于 $T2$。
也就是说,只需要考虑该串的 $half$ (长度不超过 $len/2$ 的最长回文后缀)。
证明 : $T$ 构造出半个 $S$ 的代价 : $F[T]+|S|/2-len[T]$
由于 $T2$ 是 $T1$ 的后缀,可以在 $T2$ 前面加字符得到 $T1$ ,则有 $F[T1]\leq F[T2]+len[T1]-len[T2]$
$\Rightarrow F[T1]-len[T1]\leq F[T2]-len[T2]$
$\Rightarrow F[T1]+|S|/2-len[T1]\leq F[T2]+|S|/2-len[T2]$ ,即为转移代价的大小关系,证毕。
[评测记录](https://www.luogu.com.cn/record/36564131)
- ## 更多构造方式
- **复杂度非均摊的构造 & 可持久化**
暴力跳终止链是经典构造算法复杂度需要依赖于均摊的重要原因。
回忆一下我们跳终止链是为了找什么,不难发现,在找“祖先中第一个左侧有字符 $\rm x$ 的节点”。
那么,不妨记 $u.tr[c]$ 表示 $u$ 的祖先中第一个左侧有字符 $\rm x$ 的节点。
若 $u$ 是 $v$ 的祖先,则 $v$ 继承 $u.tr$ ,并将自己左侧字符对应的 $tr$ 修改。
这样就不需要暴力跳终止链了,然而由于需要复制数组,复杂度是 $O(n\Sigma)$ 的。
若使用可持久化数组,即可实现快速创建副本,复杂度变为 $O(n\log \Sigma)$ ,也可以支持可持久化。
- **两侧同时插入**
考虑维护 $fail'$ 指针表示自动机中串的最长回文前缀。由回文,不难发现回文前缀同时也是回文后缀,所以总有 $fail'=fail$ ,不必额外维护。
所以,只需要记下正反两条终止链的末端就可以实现两侧插入了。
注意,当整个串变成回文串的时候,要更新另一侧的末端。
具体的讨论和实现请见例题 : [HDU5421 Victor and String](http://acm.hdu.edu.cn/showproblem.php?pid=5421) | [Sol](https://www.luogu.com.cn/blog/command-block/str-ji-lu-hdu5421-victor-and-string)
# 2.回文 $\bf Border$ 理论
需要先在 [Border理论小记](https://www.luogu.com.cn/blog/command-block/border-li-lun-xiao-ji) 学习前置芝士。
- **定理①** 回文串的回文后(前)缀与其 $\rm Bd$ 对应。
**证明** : 设串长为 $n$。长度为 $l$ 的回文后缀 $\Leftrightarrow\rm S[n-i+1]=S[n-l+i]=S[l-i+1]$
$\Leftrightarrow S[n-l+i-1]=S[i]\Leftrightarrow\rm $ 长度为 $l$ 的 $\rm Bd$。
图解如下,其中 $u,v$ 均为回文串,$v$ 是 $u$ 的后缀。
$\large\begin{aligned}
&\qquad\qquad\boxed{\qquad\qquad\qquad v \qquad\qquad\qquad}
\\&\boxed{\qquad\qquad\qquad\qquad u \qquad\qquad\qquad\qquad}
\\&\boxed{\qquad\qquad\qquad {\normalsize{\large v}_R}\ \ \ \quad\qquad\qquad}
\end{aligned}$
由于回文的定义 $v_R=v$ ,显然 $v$ 是 $u$ 的 $\rm Bd$。
**推论** :某个串的回文后(前)缀长度可以拆分为 $O(\log n)$ 个等差数列。
- **定理②** 若回文串 $s$ 有周期 $p$,则一个循环节必然可以被拆成两个回文串,且长度分别为 $|p|-(s\bmod p),(s\bmod p)$。
设回文串 $s$ 的长度为 $n$,循环节为 $t$,周期为 $p=|t|$。
将 $s$ 表示成 $s=t^{k}t_1$ ,其中 $t_1$ 是 $t$ 的严格前缀。令 $t=t_1t_2$。有图 :
$\begin{aligned}
&\qquad\qquad\qquad\quad\ \ \boxed{\quad{t}_1\quad}\boxed{\quad\ \,t_{2}\,\ \quad}
\\&\boxed{\qquad\quad\ t_{}\ \quad\qquad}\boxed{\qquad\quad\ t_{}\ \quad\qquad}\boxed{\quad{t}_1\quad}
\\&\boxed{\qquad\qquad\qquad\qquad\,\ \ s\ \ \ \qquad\qquad\qquad\qquad}
\\&\qquad\qquad\qquad\qquad\qquad\ \ \ \,\boxed{\qquad\quad t_R \quad\qquad}
\end{aligned}$
由上图可得 $t_R=t_1t_2$ ,且显然有 $t_R={t_2}_R{t_1}_R$ ,可得 $t_1,t_2$ 均为回文串。不难发现 $t_1,t_2$ 的长度恰为 $s-(s\bmod p),(s\bmod p)$。
- **定理③** 设 $v$ 为回文串 $u$ 的**最长**回文严格前(后)缀,若 $2|v|\geq |u|$ ,那么 $v$ 只会在 $u$ 中恰好匹配两次,分别作为前缀和后缀。
**证明** : 由 **定理①** ,显然有这两次匹配,我们需要证明没有其他的匹配。
取出前两次匹配 $v_1,v_2$ ,假设 $v_2$ 不是后缀。
由 $\rm Border$ 论, $2|v|\geq u\Rightarrow\ v$ 在 $u$ 的匹配位置成一个等差序列,且公差为 $v$ 的周期。
匹配情况如图 :
$\begin{aligned}
&\boxed{\qquad\qquad\qquad\qquad\,\ \ u\ \ \ \qquad\qquad\qquad\qquad}
\\&\boxed{\qquad\qquad v_{1}\quad|\quad\qquad}
\\&\qquad\qquad\qquad\ \boxed{\qquad\qquad v_{2}\quad|\quad\qquad}
\end{aligned}$
根据 **定理②** ,改写 $v$ 的循环节为两个回文串 $p_1,p_2$ ,则 $v=(p_1p_2)^kp_1$。
$\begin{aligned}
&\boxed{\qquad\qquad\qquad\qquad\,\ \ u\ \ \ \qquad\qquad\qquad\qquad}
\\&\boxed{\quad p_1\quad}\boxed{\ \ p_2\ \ }\boxed{\quad p_1\quad}
\\&\qquad\qquad\qquad\ \boxed{\quad p_1\quad}\boxed{\ \ p_2\ \ }\boxed{\quad p_1\quad}
\end{aligned}$
此时不难发现 $v_1∪v_2=(p_1p_2)^{2k}p_1$ 可以形成比 $v$ 更长的回文前缀,矛盾。
- **应用方式** :
具体应用中,为了利用 **定理③**,对于划分出的一个回文后缀长度等差序列 $\{a_1,a_2,...,a_i,a_{i+1},...,a_m\}$ ,若 $i$ 是第一个不满足 $2|a_i|\geq |a_{i+1}|$ 的,则从 $a_i$ 处断开成两个等差序列分别处理。
好处是,同一个等差序列的相邻串之间一定满足 **定理③**。
不难发现此时仍然能把回文后缀长度(终止链)分成 $O(\log n)$ 段序列。
- **例题** :
- [HDU6320 Cut The String](http://acm.hdu.edu.cn/showproblem.php?pid=6320) | [Sol](https://www.luogu.com.cn/blog/command-block/hdu6320-cut-the-string)
- [Loj#6681. yww 与树上的回文串](https://loj.ac/p/6681) | [Sol](https://www.luogu.com.cn/blog/command-block/str-ji-lu-loj6681-yww-yu-shu-shang-di-hui-wen-chuan)
- [CF932G Palindrome Partition](https://www.luogu.com.cn/problem/CF932G) | [Sol](https://www.luogu.com.cn/blog/command-block/str-ji-lu-cf932g-palindrome-partition)
- [Loj#6070. 「2017 山东一轮集训 Day4」基因](https://loj.ac/problem/6070) | [Sol](https://www.luogu.com.cn/blog/command-block/str-ji-lu-loj6070-2017-shan-dong-yi-lun-ji-xun-day4-ji-yin)