回文自动机(PAM) 详解
PAM 是一种高效存储字符串中所有回文子串的自动机,用于解决回文串相关问题。
虽然代码稍微长一点,但写起来比 manacher 容易很多,毕竟没有加了一堆字符再转回原串的若干上取整下取整问题。
前置知识
无。或许需要一些自动机相关的理论基础。
结构 & 定义
状态
我们用 PAM 上的一个节点来表示一个回文子串,作为 PAM 的一个状态。但回文串分奇偶两种,像 manacher 一样在每两个字符之间加分隔符是很麻烦的。因此,我们把 PAM 的状态分成两个部分,一部分存奇回文串,另一部分存偶回文串。
同理,我们把根也分为奇根和偶根。它们不表示任何字符串,只作为初始状态而存在。
因为存的是回文串,我们其实只需要对一个串记录其中一半位置的字符是什么,所以定义 PAM 上的一个点到根的路径上的字符串表示它所代表的回文串的其中一半,这一点上 PAM 与以前学过的自动机状态的定义都不同。
换句话说,对于一个点它实际表示的回文串,在 PAM 上的读法是从它开始沿着 PAM 读到根,再原路读回该点形成的字符串。这里注意如果是奇回文串,与根相连的那个字符边只读一次。
令奇根为 \(1\),偶根为 \(0\)。那么对于 \(s=\texttt{"abaabc"}\),建出的 PAM 如下图(省略 Fail 指针):

每个点表示的回文串根据上文所述即可读出,例如点 \(7\) 表示 \(\texttt{c}\),点 \(4\) 表示串 \(\texttt{aba}\),点 \(6\) 表示串 \(\texttt{baab}\)。
Fail 指针
在 PAM 中,每个节点同样有一个 Fail 指针。这里 \(\text{Fail}(x)\) 的定义是 \(x\) 表示的回文串的最长回文后缀的状态。(也同时是最长回文前缀,因为 \(x\) 是回文串)
对于初始状态,我们定义偶根的 Fail 指向奇根。而我们并不关心奇根的 Fail,因为任意一个长度为 \(1\) 的字符串都是回文串,奇根不可能失配。
同时,对于每个节点我们记录 \(\text{len}(x)\) 表示它实际代表的回文串长度,用于在下文 PAM 的构造中判断每个点在原串中的位置。那么对于点 \(x\),设它在 PAM 上的父亲是 \(fa(x)\),则有 \(\text{len}(x)=\text{len}(fa(x))+2\)。
为保证奇回文串也满足这个性质,令 \(\text{len}(1)=-1\)。
对于之前的例子,建出它的 Fail 指针就是这样的(为了减少交叉,把 \(2\) 和 \(7\) 号点换了位置):

PAM 的构造
PAM 的构造方式是在线的,即每次添加一个字符 \(c\) 时,在原来的 PAM 基础上添加与新增字符相关的状态和转移。
假设对于一个长度为 \(n\) 的字符串 \(s\),我们已经构造完了前 \(n-1\) 个字符,现在要加入第 \(n\) 个字符。设这个字符为 \(c\),前 \(n-1\) 个字符最长回文后缀对应的状态是 \(now\)。
要新增的状态就是以第 \(n\) 个字符结尾,且在以前没有出现过的回文串。考虑到一个回文串前后各去掉一位还是回文串,新增的回文串前后去掉一位,一定是某个以 \(n-1\) 为结尾的回文串。
我们要找的就是以 \(n-1\) 为结尾的回文串中,前一位恰好是 \(c\) 的最长的串。发现上一次构造到的节点 \(now\) 就表示以 \(n-1\) 为结尾的最长回文串,因此我们不断令 \(now\gets \text{Fail}(now)\),直至满足条件。设使条件满足的状态为 \(pos\)。那么以 \(n\) 为结尾的最长回文后缀就是在 \(x\) 前后各加一个 \(c\)。根据 PAM 的定义,这个状态就是 \(pos\) 通过字符 \(c\) 的边连向的儿子。
如果 \(pos\) 没有对应的儿子,代表这个回文串是新增的,我们添加一个新的状态。否则既然已经存在就不用管了。
那么对于这个新的状态,可以证明只有最长的这个回文后缀是新增的。考虑一个比它短的回文后缀,可以在最长的里面对称到一个不包括 \(n\) 的字符串,这一定是以前出现过的状态。

(如果黑的是最长回文后缀,蓝的是次长回文后缀,那显然红的和蓝的相同,也是回文串,在前面出现过。)
那么我们令新点为 \(new\),容易得出 \(fa(new)=pos\),\(\text{len}(new)=\text{len}(pos)+2\)。
只剩下它的 Fail 还没有求了。
发现求 Fail 指针和求最长以 \(n\) 结尾的回文后缀的本质是一样的。这次我们从 \(\text{Fail}(pos)\) 开始跳,找到第一个能在前后各加一个 \(c\) 的回文串即可。
如果到最后都没匹配到,令 \(\text{Fail}(new)=0\) 就好了。
你可能会奇怪为什么不指向奇根呢,这两个都是啥也没有。考虑这样的情况:原来字符串只有一个字符 \(\texttt{a}\),现在变成了 \(\texttt{aa}\)。那么这个新回文串是从偶根同时也是之前的 Fail 转移过来的。如果一开始把 \(a\) 的 Fail 指向奇根就会转移不到这种情况。
看起来构建完了。
实现
结构体定义是平凡的:
struct node{int len,fa,s[26];} d[N];
设函数 getfail(u,i) 表示从状态 \(u\) 开始查找首个前一位与 \(s_i\) 相同的回文后缀状态。
il int getfail(int u,int i)
{
while(i-d[u].len-1<=0||s[i-d[u].len-1]!=s[i]) u=d[u].fa;
return u;
}
具体的构造根据上文也不难写出。
d[0].fa=1,d[1].len=-1;
for(int i=1,now=0;i<=n;i++)
{
int pos=getfail(now,i),c=s[i]-'a';
if(!d[pos].s[c])
{
d[++tot].fa=d[getfail(d[pos].fa,i)].s[c];
d[pos].s[c]=tot,d[tot].len=d[pos].len+2;
}
now=d[pos].s[c];
}
注意一定要先对新点求 Fail,再连新的转移边。
考虑颠倒顺序会出什么问题,上文说过“最后匹配不到,要令 Fail=0”,正常情况下匹配不到,\(pos\) 就没有 \(c\) 的转移边,Fail 确实是 \(0\);而如果先给 \(pos\) 连了新节点的边,最后匹配不到,新节点就会把 \(Fail\) 连到自己,喜提死循环。
看起来也实现完了。
正确性证明
接下来对 PAM 的时空复杂度均为线性给出证明。(因为这个还比较好证,那就写一下。
状态数证明
PAM 的状态数即为字符串本质不同的回文子串个数。
考虑每次新增一个字符,上文已经证明过只有以它结尾的最长回文后缀可能是新增的。也就是说,每次新增字符,本质不同的回文子串数至多增加 \(1\)。
因此任意字符串本质不同回文子串数不多于 \(n\),上界可以在形如 \(s=\texttt{"aaaa}\dots\texttt{aaaa"}\) 的字符串中取到。
时间复杂度证明
看起来除了我们每次匹配都在循环跳 Fail 指针以外,线性复杂度都很显然。
考虑 \(now\) 在 Fail 树上的深度,每次新增一个字符至多使它的深度增加 \(1\)。那么 \(n\) 个字符至多增加了 \(n\) 次,\(now\) 至多跳了 \(2n\) 次 Fail。
而求 Fail 指针时的那个循环也是同理的。因此 PAM 的时间复杂度是线性。
例题
P5496【模板】回文自动机(PAM)
根据 Fail 的定义,发现对于 PAM 上的一个状态,以它结尾的回文后缀个数就是它在 Fail 树上的深度。
那么我们只需要在构建过程中记录每个状态在 Fail 树上的深度即可。
点击查看代码
const int N=5e5+5;
int n,tot=1;
struct node {int len,fa,dep,s[26];}d[N];
char s[N];
il int getfail(int u,int i)
{
while(i-d[u].len-1<=0||s[i-d[u].len-1]!=s[i]) u=d[u].fa;
return u;
}
int main()
{
scanf("%s",s+1); n=strlen(s+1);
d[0].fa=1,d[1].len=-1;
int now=0,lst=0;
for(int i=1;i<=n;i++)
{
s[i]=(s[i]-'a'+lst)%26+'a';
int pos=getfail(now,i); int c=s[i]-'a';
if(!d[pos].s[c])
{
d[++tot].fa=d[getfail(d[pos].fa,i)].s[c];
d[pos].s[c]=tot,d[tot].len=d[pos].len+2;
d[tot].dep=d[d[tot].fa].dep+1;
}
now=d[pos].s[c];
printf("%d ",lst=d[now].dep);
}
return 0;
}
这题要记录每个回文串出现的次数。在 Fail 树的角度考虑,每加入一个字符,最长回文后缀所在的状态在 Fail 树上的所有祖先出现次数都 \(+1\)。
显然每次都跳 Fail 暴力修改是不可行的,因此只在最长回文后缀处打一个标记,最后统计答案时,状态的出现次数就是 Fail 树的子树和。
点击查看代码
const int N=3e5+5;
int n,tot=1;
long long f[N];
char s[N];
struct node{int len,fa,s[26];} d[N];
il int getfail(int u,int i)
{
while(i-d[u].len-1<=0||s[i-d[u].len-1]!=s[i]) u=d[u].fa;
return u;
}
int main()
{
scanf("%s",s+1); n=strlen(s+1);
d[0].fa=1,d[1].len=-1;
for(int i=1,now=0;i<=n;i++)
{
int pos=getfail(now,i),c=s[i]-'a';
if(!d[pos].s[c])
{
d[++tot].fa=d[getfail(d[pos].fa,i)].s[c];
d[pos].s[c]=tot,d[tot].len=d[pos].len+2;
}
now=d[pos].s[c],f[now]++;
}
long long ans=0;
for(int i=tot;i;i--)
{
f[d[i].fa]+=f[i];
ans=max(ans,1ll*f[i]*d[i].len);
}
printf("%lld\n",ans);
return 0;
}
P4199 万径人踪灭
ybtoj 翻到的。其实这题重点不在 PAM/manacher,倒不如说是 FFT 练习题(
答案可以转化成所有合法子序列减掉连续的。
对于不保证连续的,发现本质是求下标和等于某个数(即关于某个点对称)且相同的点对数。这个东西形如多项式卷积,对两种字符分别 FFT。
而连续回文子串的总数可以用 PAM 来求,代码不放了。
回文自动机(PAM) 详解的更多相关文章
- 回文自动机pam
目的:类似回文Trie树+ac自动机,可以用来统计一些其他的回文串相关的量 复杂度:O(nlogn) https://blog.csdn.net/Lolierl/article/details/999 ...
- 回文树(回文自动机PAM)小结
回文树学习博客:lwfcgz poursoul 边写边更新,大概会把回文树总结在一个博客里吧... 回文树的功能 假设我们有一个串S,S下标从0开始,则回文树能做到如下几点: 1.求串S前缀0~ ...
- 回文树/回文自动机(PAM)学习笔记
回文树(也就是回文自动机)实际上是奇偶两棵树,每一个节点代表一个本质不同的回文子串(一棵树上的串长度全部是奇数,另一棵全部是偶数),原串中每一个本质不同的回文子串都在树上出现一次且仅一次. 一个节点的 ...
- 回文自动机(PAM) 入门讲解
处理回文串,Manacher算法也是很不错,但在有些问题的处理上比较麻烦,比如求本质不同的子串的数量还需要结合后缀数组才能解决.今天的们介绍一种能够方便的解决关于回文串的问题的算法--PAM. 一些功 ...
- 洛谷P5496 回文自动机【PAM】模板
回文自动机模板 1.一个串的本质不同的回文串数量是\(O(n)\)级别的 2.回文自动机的状态数不超过串长,且状态数等于本质不同的回文串数量,除了奇偶两个根节点 3.如何统计所有回文串的数量,类似后缀 ...
- HDU6599 (字符串哈希+回文自动机)
题意: 求有多少个回文串的前⌈len/2⌉个字符也是回文串.(两组解可重复)将这些回文串按长度分类,分别输出长度为1,2,...,n的合法串的数量. 题解:https://www.cnblogs.co ...
- 【XSY2715】回文串 树链剖分 回文自动机
题目描述 有一个字符串\(s\),长度为\(n\).有\(m\)个操作: \(addl ~c\):在\(s\)左边加上一个字符\(c\) \(addr~c\):在\(s\)右边加上一个字符 \(tra ...
- 字符串数据结构模板/题单(后缀数组,后缀自动机,LCP,后缀平衡树,回文自动机)
模板 后缀数组 #include<bits/stdc++.h> #define R register int using namespace std; const int N=1e6+9; ...
- bzoj千题计划306:bzoj2342: [Shoi2011]双倍回文 (回文自动机)
https://www.lydsy.com/JudgeOnline/problem.php?id=2342 解法一: 对原串构建回文自动机 抽离fail树,从根开始dfs 设len[x]表示节点x表示 ...
- 【回文自动机】bzoj3676 [Apio2014]回文串
回文自动机讲解!http://blog.csdn.net/u013368721/article/details/42100363 pam上每个点代表本质不同的回文子串.len(i)代表长度,cnt(i ...
随机推荐
- 记一次 .NET 在线客服系统同时支持 SQL Server 和 MySQL 没卡死分析
前段时间我发表了一系列文章,开始介绍基于 .net core 的在线客服系统开发过程. 有很多朋友一直提出希望能够支持 MySQL 数据库,考虑到已经有朋友在用 SQL Server,我在升级的过程中 ...
- 微信小程序 - 视图与逻辑
[黑马程序员前端微信小程序开发教程,微信小程序从基础到发布全流程_企业级商城实战(含uni-app项目多端部署)] https://www.bilibili.com/video/BV1834y1676 ...
- 【Springboot】SpringBoot-Admin 服务监控+告警通知
SpringBoot-Admin 服务监控 简单介绍 Spring Boot Actuator 是 Spring Boot 自带的一个功能模块, 提供了一组已经开箱即用的生产环境下常用的特性和服务,比 ...
- dash构建多页应用
dash 构建多页面应用一种方案 本方案对dash官网多页面案例使用dash_bootstrap_components案例进行优化与测试,效果如下 项目代码结构如下 │ app.py │ ├─asse ...
- Builder 生成器模式简介与 C# 示例【创建型2】【设计模式来了_2】
〇.简介 1.什么是生成器模式? 一句话解释: 在构造一个复杂的对象(参数多且有可空类型)时,通过一个统一的构造链路,可选择的配置所需属性值,灵活实现可复用的构造过程. 生成器模式的重心,在于分离 ...
- python-gitlab 一个简单demo
背景 需要收集git仓库信息到数据库供前端展示 包括:仓库信息.仓库所有者.成员列表.提交信息.活跃情况等 需要定时启动.灵活触发 实现简介 使用gitlab v4 restful 接口 使用pyth ...
- RedHat8静默安装was
前言 was(websphere application server),类似weblogic.tomcat,由IBM开发的一种企业级Java容器. 系统版本:redhat 8.2 was版本:was ...
- 基于weave实现docker跨主机网络通信
前言 IP: 192.168.0.10 192.168.0.11 系统版本:centos 7 weave版本:2.8.1,下载地址:https://git.io/weave docker版本:18.0 ...
- 你们眼睛干涩,胀痛吗?C# WPF 久坐提醒桌面小程序
目录 说明 设置提醒时间,及休息时间 久坐提醒倒计时 休息提醒倒计时 休息到计时 代码说明 主窗体设置 工作到计时 休息倒计时 源码 久坐提醒桌面小程序: 干这行职业病比较多,之前用爱丽(即:玻璃酸钠 ...
- c# 如何将程序加密隐藏?
下面将介绍如何通过LiteDB将自己的程序进行加密,首先介绍一下LiteDB. LiteDB LiteDB是一个轻量级的嵌入式数据库,它是用C#编写的,适用于.NET平台.它的设计目标是提供一个简单易 ...