回文自动机(PAM)学习笔记
Update:2022.7.28。补充了对模板题解法的进一步说明,并对某些结论进行了补充或证明。
前言
想必各位都学过 Manacher 算法(没关系,没学过不影响今天的内容)。不得不承认,Manacher 的确是处理回文问题 一个强力工具,可以在
但是,对于回文计数类问题(比如求一个字符串有多少个本质不同的回文子串),Manacher 就心有余而力不足了。所以,我们需要一个更为强大的数据结构处理这个问题。这就是今天的回文自动机(PAM)。
概念
作为一个数据结构,首先需要明确回文自动机是个什么东西。
在开始之前,需要明确一些符号:
回文自动机是一棵树(因此也叫作回文树),它存的是一个字符串中的所有回文子串 的 一半。
而由于回文串又分奇回文和偶回文,所以回文自动机有两个根节点,
而它的每条边,代表一个字符
什么意思呢?我们不妨来看张图。
以字符串
- 奇数:
a,b,aba,bab,ababa ; - 偶数:
aa,baab,abaaba,babaabab,ababaababa 。
它们建出来的回文自动机是这样的:
有没有发现回文自动机和 Manacher 的关系很像 AC 自动机与 KMP 的关系,都是爸爸和儿子。
从图中我们得到两个结论:
- 除根节点外,每个节点恰好对应一个本质不同的回文子串,所以该树节点数
-2 就是本质不同的回文子串个数(这很显然,原因跟 Trie 的储存原理是一样的,互不相同)。 - 若
0 号节点对应字符串长度为0 ,1 号为-1 。则其余节点深度每+1 ,所对应的回文串长度+2 (因为从下到上读一遍,又从上往下又读一遍)。
所以你现在知道为什么只存回文的一半了吧?除了节省空间,还是为了避免本质相同的子串。
原理
现在我们来看看回文自动机是怎么工作的。
插入新节点
这个地方我看其他讲解的的时候懵懵懂懂的,希望这里能讲明白。
我们考虑轮到
显然,加入该字符时肯定会出现若干个新的回文子串。那是不是要枚举所有新出现的回文子串呢?
显然不是。回文串左右两边相等。当插入第
以下是不严谨的证明:首先我们需要明确,新增的所有回文子串必定是当前串的后缀(因为它们全都包含当前的
说穿了,精髓就是长回文串一定包含短回文串。
所以回文自动机的空间复杂度是
例如
现在,我们就要考虑如何将
首先,
怎么找这个
那么,由这个“最长回文后缀”你想到了什么?
没错,AC 自动机的
为什么是非自身?如果加上自身,它的 然后你的程序就会 T 成二臂……
记
下面是完整的回文自动机的图:
最后一个问题:为什么
最后,后缀自动机的时间复杂度为 xjb 分析:首先,
Solution
讲完原理,看看这道题怎么做:P5496 【模板】回文自动机(PAM)。
这题要求我们输出以字符串每个位置结尾的回文子串个数。因为回文自动机本身就涉及到后缀,所以要求结尾个数,只需要定义
@管理员大大 这个题没啥好分析的吧,弄懂原理就很简单了。
最后就是代码了,细节都在注释中。
#include<bits/stdc++.h>
using namespace std;
const int N = 5e5 + 10;
char s[N];
int n, lst, len[N];
int now, tot = 1, fail[N], cnt[N], t[N][26];
//tot 记得赋成 1,已经有两个根节点了
int getfail(int u, int p){//求 u 的fail指针 ,p表示当前插入的是哪个点
while(p - len[u] - 1 <= 0 || s[p - len[u] - 1] != s[p]) u = fail[u];
//如果当前位置比 u的回文串短或者字符不相等 就一直跳
return u;//最后不跳了,你就是答案了
}
int insert(char c, int id){
int p = getfail(now, id);//找到那条路中有 id 位都一样的最长回文子串
//这个点就是他爸爸
if(!t[p][c - 'a']){//爸爸没它这个儿子,新认一个
fail[++tot] = t[getfail(fail[p], id)][c - 'a'];
//fail等于((((爸爸的fail那条路上)有 id 位都是一样的回文串)的节点)的同名儿子)
t[p][c - 'a'] = tot;//跟trie树一样
len[tot] = len[p] + 2;//长度等于爸爸+2
cnt[tot] = cnt[fail[tot]] + 1;//回文串数量等于fail的数量加上它自己
}
return cnt[now = t[p][c - 'a']];//更新它现在的位置,并返回cnt作为答案
}
int main(){
scanf("%s", s + 1);
n = strlen(s + 1);
fail[0] = 1, len[1] = -1;
//len[i] 表示节点 i 所对应的回文串长度
for(int i=1;i<=n;i++){
if(i > 1) s[i] = (s[i] - 'a' + lst) % 26 + 'a';
printf("%d ", lst = insert(s[i], i));
}
return 0;
}
这代码只能说一个字:短。
秒完这题,大家也可以去看看 P4287 双倍回文 和其他一些比较基础的题目,加深理解。
到此结束吧,记得留下你们的赞哦!