- admin 的博客
回文自动机小记
- @ 2026-7-10 10:58:49
回文自动机小记 更新于 2026/6/26 20:31:06 作者
command_block
1.基本元素与构造
-
回文串
约定 表示将字符串 翻转形成的字符串。
满足 的字符串 称之为回文串。
本文编写历程时间跨度较大,由于前后习惯不同,可能混用大小写字母表示字符串。
-
回文自动机的基本元素
每个节点表示原串的一个回文子串,不同的节点表示的回文子串本质不同。
转移边表示给当前串左右同时加上某个字符。若有 ,则称 为 的前驱, 为 的后继(之一)。
边指向自己的最长回文后缀,不难发现 形成一棵有根内向树。
称从结尾在原串末尾的最长回文子串开始向上跳 得到的链为终止链,其在原串中对应着这样的一系列回文子串。

即原串的所有回文后缀。
-
回文自动机的经典构造方法
增量法构造,每次在原串末尾加入一个字符,不妨设为 。
考虑加入 后的新回文后缀,如下图 :

-
性质① 每次最多只会加入一个新的本质不同的回文串,且是所有回后缀中最长的。
如图,红色串为最长回文后缀,绿色串为某个较短的回文后缀。

则根据回文的性质,绿色串必然在之前已经出现过了。
推论 :本质不同的回文子串(回文自动机的点数)不超过 。
现在,问题就是如何找到这个最长的新回文后缀。
对于每个新回文后缀,若在其两侧去掉一个 ,就是一个旧的回文后缀。
考虑新出现的回文串(红色),则绿色部分(前驱)为旧终止链中的某个串。

那么,可以暴力跳旧终止链,并检查对应回文串左侧是否恰好有一个 。
由于终止链上长度是由下而上递减的,一旦找到一个符合要求的点 ,即找到了最长的新回文后缀。
若这个 已经有 出边,则表示该最长回文后缀已经在前面出现过,找出 ,本次终止链末尾即为 。
否则,最长回文后缀是全新的,新建点 装这个后缀,连一条 ,且 。
还有一个问题是,该串的 指向的新次长回文后缀如何寻找。
可以顺着终止链继续找到旧的回文后缀 满足左边有一个 。
根据 性质① 的证明,新次长回文后缀在前面出现过,所以此时 必有 出边。
找出 ,则 。
-
复杂度证明
每次加入字符时,只会新增一个点和一个转移,状态总量显然是 的。
每次暴力跳终止链的复杂度令人怀疑,其实也是 的。
设当前终止链长度为 ,若跳过 步才找到 ,则新的由 开始的终止链长度为 。
终止链长度的消长相当于蜗牛爬杆,复杂度是正确的均摊 。
-
实现细节
奇回文串的构造并不完全遵守“左右同时加上某个字符”的规则,中心字符是单独出现的。
所以,需要建立奇偶两个根,规定奇根转移得出的都是奇回文串,偶根转移得出的都是偶回文串。
对奇根采取特殊规则,走转移只会增加一个字符,即为某个奇回文串的重心字符。
为了兼容,可以把奇根的 置为 (这样可以看作加入两个重叠的字符)。
偶根的 为奇根 (奇根的限制比偶根少,若没有偶回文串,尝试一下奇回文串的最低要求 : 单个字符) ,奇根的 指向自己。
加入字符 的构造过程中,如果发现串没有新次短回文后缀,则意味着前面的串中根本没有字符 ,此时将最长回文后缀(只有一个字符 ) 的 置为偶根。
可能父亲和出边到自己的点是同一个点,要注意赋值顺序问题。
此外,为了避免越界,让字符串从 开始标号,并把第 个字符标记为 。
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
-
经典应用
-
本质不同回文串数
点数 (除去两个根)
- 各个回文子串的出现次数
构造过程中,每加入一个字符时,相当于把整条终止链的出现次数 。
那么我们把每次的终止链末尾权值 ,最后在 树上求和即可。
实际操作中,由于编号大的点一定不会是编号小的点的父亲,不需要建立树,直接按照编号求和也可。
求出出现次数之后,乘上 取 即可。
本题相当于在线地询问终止链长度,记录 并递推。
注意到双倍回文串也是一个回文串,此题可以转化成对于每个回文串,判定是否能拆成两个相同的偶回文串。
可以从该点跳 找到自己的所有回文后缀。若有长度恰为 的,则符合要求。
暴力跳复杂度显然不对劲。
建立 树, 时开一个桶记录到根的链中各个点的 ,对于每个点只需要查看桶中为 的位置。
对于每个位置,记录在此结束和开始的最长回文串长度,然后拼接即可。
可以正反分别构造一次回文自动机来求出。
可以转化为求不相交的回文串对子数。
枚举靠前的串的结尾位置 ,则靠后的串只需要在 以后开始即可。
记录 为在 开始的回文串个数, 为结束。(即为终止链长度)
可以正反构造两次回文自动机来求出。
$\sum\limits_{i=1}^{|S|}R[i]\sum\limits_{j=i+1}^{|S|}L[j]$ 即为答案。
其实就是双串最长公共回文子串。
对两个串分别建立回文自动机,由于状态量很少,可以选取两个自动机相同的转移进行搜索,即可得到所有的公共回文子串。
题意就是求两个字符串的相等回文子串对子数。
类似上一题,对同样的转移搜索,将对应节点的出现次数相乘求和。
另一种做法是,先建立 的 ,然后用 在上面跑匹配。
逐个加入 中的字符,维护在 中能匹配到的最长回文后缀。
注意,匹配的时候是双向扩展,除了检查自动机的出边外,还要检查 串对应的两个字符是否相等。
若匹配到 点,则可以视为包含到了所有 的祖先,对次数的贡献就是这一条链的 和。
那么预先向下求和即可。
原题 ,利用 可以做到 。
记 中串 长度不超过 的最长回文后缀为 。
在构造回文自动机的同时,若新产生的点为 ,且找到 ,那么先找到 ,然后向祖先跳直到能产生对应回文后缀为止(注意不能直接检查有无出边,就算有也可能只是前面的)。
此时出边指向的那个点就是 。复杂度证明类似前面对终止链长度变化的分析。
记 表示回文串 的阶数,有如下 :
$\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}$
能够发现,答案肯定是在某次 操作之后暴力加字符得来的。
我们建立原串的回文自动机,然后分别求出每个(偶)回文子串 的构造代价 ,原串的构造代价即为 。
考虑如何构造一个回文串,不难发现,最优策略的最后一步操作一定是 。
看看上一次的 是怎么用的。
如果用在外面,则可以视为在两侧同时加了一个字符,则可以按照从后缀自动机的转移边来 。
转移为
如果用在里面,则必然先造好一个后缀,这里只需要考虑回文后缀。
设 为目标, 为两个能够转移的回文后缀,若 则 不劣于 。
也就是说,只需要考虑该串的 (长度不超过 的最长回文后缀)。
证明 : 构造出半个 的代价 :
由于 是 的后缀,可以在 前面加字符得到 ,则有
$\Rightarrow F[T1]+|S|/2-len[T1]\leq F[T2]+|S|/2-len[T2]$ ,即为转移代价的大小关系,证毕。
-
更多构造方式
-
复杂度非均摊的构造 & 可持久化
暴力跳终止链是经典构造算法复杂度需要依赖于均摊的重要原因。
回忆一下我们跳终止链是为了找什么,不难发现,在找“祖先中第一个左侧有字符 的节点”。
那么,不妨记 表示 的祖先中第一个左侧有字符 的节点。
若 是 的祖先,则 继承 ,并将自己左侧字符对应的 修改。
这样就不需要暴力跳终止链了,然而由于需要复制数组,复杂度是 的。
若使用可持久化数组,即可实现快速创建副本,复杂度变为 ,也可以支持可持久化。
- 两侧同时插入
考虑维护 指针表示自动机中串的最长回文前缀。由回文,不难发现回文前缀同时也是回文后缀,所以总有 ,不必额外维护。
所以,只需要记下正反两条终止链的末端就可以实现两侧插入了。
注意,当整个串变成回文串的时候,要更新另一侧的末端。
具体的讨论和实现请见例题 : HDU5421 Victor and String | Sol
2.回文 理论
需要先在 Border理论小记 学习前置芝士。
- 定理① 回文串的回文后(前)缀与其 对应。
证明 : 设串长为 。长度为 的回文后缀
长度为 的 。
图解如下,其中 均为回文串, 是 的后缀。
$\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}$
由于回文的定义 ,显然 是 的 。
推论 :某个串的回文后(前)缀长度可以拆分为 个等差数列。
- 定理② 若回文串 有周期 ,则一个循环节必然可以被拆成两个回文串,且长度分别为 。
设回文串 的长度为 ,循环节为 ,周期为 。
将 表示成 ,其中 是 的严格前缀。令 。有图 :
$\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}$
由上图可得 ,且显然有 ,可得 均为回文串。不难发现 的长度恰为 。
-
定理③ 设 为回文串 的最长回文严格前(后)缀,若 ,那么 只会在 中恰好匹配两次,分别作为前缀和后缀。
证明 : 由 定理① ,显然有这两次匹配,我们需要证明没有其他的匹配。
取出前两次匹配 ,假设 不是后缀。
由 论, 在 的匹配位置成一个等差序列,且公差为 的周期。
匹配情况如图 :
$\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}$
根据 定理② ,改写 的循环节为两个回文串 ,则 。
$\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}$
此时不难发现 可以形成比 更长的回文前缀,矛盾。
-
应用方式 :
具体应用中,为了利用 定理③,对于划分出的一个回文后缀长度等差序列 ,若 是第一个不满足 的,则从 处断开成两个等差序列分别处理。
好处是,同一个等差序列的相邻串之间一定满足 定理③。
不难发现此时仍然能把回文后缀长度(终止链)分成 段序列。
-
例题 :