马拉车(manacher) & 回文自动机(PAM)
补充,PAM 的 a[0]=-1,这一点我每次写都要忘记。
读了徐安矣2023年集训队论文写的,对于差分性质和习题,我会在理解清楚之后再补充。本篇博客仅讨论前两种算法。
首先,马拉车和回文自动机都是处理回文串问题的。但在此之前,学习一些更加简单的回文算法。
小 trick:把给定串的两头和缝隙插入相同字符,且在边界处用不同字符标记,使得长度为偶数的回文串和长度为奇数的回文串可以用同一种方式算出来。
例子:
ABBABAB \(\to\) @#A#B#B#A#B#A#B#%
其中的 ABBA \(\to\) #A#B#B#A#,回文中心为中间的 #。ABA \(\to\) #B#A#B#,回文中心为 A。
下文的 \(N\) 表示字符串长度。除了回文自动机使用原串外,其它算法都需要使用在缝隙添加相同字符的新字符串。
- 暴力
对于一个回文中心,枚举回文半径。时间复杂度为 \(O(n^2)\)。
- 哈希
预处理前缀哈希和后缀哈希,可以对于一个回文中心,二分回文半径。具体原理就是如果二分到 Mid,i 左右的长度为 Mid 的段的哈希值相等,那就是回文的,反之则不回文,有二分性。时间复杂度,预处理 \(O(n)\),求单个点的回文半径 \(O(\log n)\)。
- 马拉车
记录当前右端点最大的回文串的回文中心的右端点。初始都是 \(0\)。枚举回文中心,从 \(1\) 到 \(N\)。
如果当前回文中心在右端点之后,那根据每个回文串右端点至少为其本身的性质,当前最右右端点一定为 \(i-1\),则直接暴力更新这个点为回文中心的回文串的右端点,顺带可以算出这个点的回文半径。
否则,根据回文的性质,设当前最右端点为 \(MaxR\),其回文中心为 \(MaxI\),则 \(i\) 可以通过 \(MaxI\) 映射到 \(2MaxI-i\) 这个点,因为这是回文的。之前已经计算过 \(2MaxI-i\) 的回文半径了,但由于在 \(MaxI\) 为回文半径的回文串左端点之前的值与 \(MaxR\) 之后的值不能映射(因为不回文了),若 \(R_i\) 表示 \(i\) 的回文半径,则 \(i\) 点的初始回文半径为 \(\min(R_{2MaxI-i},MaxR-i+1)\)。
定义一下回文半径,若 \(i\) 的回文半径为 \(R_i\),则以 \(i\) 为回文中心的极长回文串为 \([i-R_i+1,r+R_i-1]\),极长的意思就是再长就不行了。
上文的初始回文半径可能并不是最终回文半径,所以暴力拓展,在拓展的同时,顺带更新 \(MaxR\) 即可。因为想要暴力拓展,当且仅当 \(i\) 点的初始回文半径为 \(MaxR-i+1\),此时当前回文串的右端点即为 \(MaxR\),所以 \(MaxR\) 可以跟 \(R_i\) 同时扩展。
考虑时间复杂度,枚举 \(i\) 这部分的时间复杂度为 \(O(N)\),\(MaxR\) 最多只会增加到 \(N\),所以均摊时间复杂度也是 \(O(N)\)。总时间复杂度为 \(O(N)\)。是最快的处理每个点的回文半径的算法。
- 回文自动机
说到这个,我以后还会写 AC 自动机和后缀自动机,优先度很高。如果还有多余的时间,可以写子序列自动机。
回文自动机可以理解成,既是一个 DAG,又是一棵树,其中每个点表示一个回文串。其中 DAG 每条边最多有字符集大小条出边,每条出边表示一个字符。走一条边相当于把一个回文串两边同时加上这条边对应的字符。回文自动机上的每个点的回文串都是原字符串的本质不同回文子串。
为了方便表示,设字符集大小为 \(|S|\)。树上节点 \(u\) 的父亲用 \(Fail_u\) 表示。
首先抛出来一个非常厉害的结论,若在一个串最后添加一个字符,这个串最多只会新增一个回文串。因为如果新增 \(\geq 2\) 个回文串,右端点肯定都是新增那个字符。然后这些回文串的回文中心一定有先后关系,最前面的那个回文串(即最长后缀回文串)可以把后面的回文串给映射到前面去。既然后面是回文的,所以映射到前面去跟后面是相同的,所以后面的回文串一定在前面出现过。所以没出现过的新增回文串只有可能有一个,且为最长后缀回文串。
根据上面这个性质,可以知道,每个字符串的本质不同回文子串的数量级是 \(O(n)\) 的,所以回文自动机的点数和边数有保证。\(Fail_u\) 的实际意义则为:\(u\) 这个回文串的最长回文后缀(除去自己以外的)。
考虑增量法构造回文自动机。已经构造好当前字符串的回文自动机了,新增一个字符 \(c\),然后变成新的回文自动机。开一个数组 \(Length\),其中 \(Length_u\) 表示 \(u\) 这个回文串的长度。方便更新回文自动机。设边的集合为 \(Next\),\(Next_{u,i}\) 表示点 \(u\) 的字符为 \(i\) 的出边连向的点。
初始有两个点,\(0\) 号点,\(Length=0\),表示空回文串,\(1\) 号点,\(Length=-1\),表示左右各添加一个相同字符后成为一个长度为 \(1\) 的回文串。\(Fail_0=1\),方便写代码。用全局变量 \(End\) 表示当前字符串的最长回文后缀,初始为 \(0\)。
新增的点,从最长回文后缀开始跳 \(Fail\),直到遇到一个回文串(设为 \(u\))满足原串中这个回文串左边的第一个字符为 \(c\),这样就满足加入 \(c\) 后,最长回文后缀为那个回文串左右各添加一个 \(c\)。若那个字符串已经有一条 \(c\) 的边了,说明当前没有新增回文串,则 \(End=Next_{u,c}\),没了。否则就新建一个点,再连上这条边。再从 \(Fail_u\) 开始找最长回文后缀满足在原串上左边为 \(c\) 的回文串,设为 \(v\)。则 \(Fail_{new}=Next_{v,c}\)。根据上面的结论,这条边 \(Next_{v,c}\) 一定是存在的。
注意特殊情况,即 \(u\) 为 \(1\) 时,即最长回文后缀就是这个字符本身,那么 \(Fail_{new}=0\)。在写代码时,可以通过全局数组初始值全为 \(0\) 的特性来让代码更简洁,而不需要特判,具体见代码。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN=1e6+50;
char s[MAXN],a[MAXN];
int N;
int tot;
int Next[MAXN][26],Fail[MAXN],Length[MAXN];
int End;
long long Cnt[MAXN];
int GetFail(int u,int Id)
{
while(a[Id-Length[u]-1]!=a[Id])
u=Fail[u];
return u;
}
int main()
{
a[0]=-1;
Length[1]=-1;
Fail[0]=1;
tot=1;
scanf("%s",&s[1]);
N=strlen(s+1);
for(int i=1;i<=N;i++)
{
a[i]=s[i]-'a';
int Last=GetFail(End,i);
if(Next[Last][a[i]]==0)
{
tot++;
Fail[tot]=Next[GetFail(Fail[Last],i)][a[i]];
Length[tot]=Length[Last]+2;
Next[Last][a[i]]=tot;
}
End=Next[Last][a[i]];
Cnt[End]++;
}
long long Max=0;
for(int i=tot;i>=2;i--)
{
Max=max(Max,1ll*Length[i]*Cnt[i]);
Cnt[Fail[i]]+=Cnt[i];
}
printf("%lld",Max);
}
时间复杂度为 \(O(N)\),因为跳 \(Fail\) 可以看做,每次使得深度加一,然后再使得深度变浅很多,所以也是 \(O(N)\) 级别的。
可以快速得到的东西很多,比如想要知道当前字符串的本质不同回文子串数量,则为除了 \(0\) 和 \(1\) 这两个点之外的点数。遍历一遍即可得到所有回文串,得到回文中心和半径也就很简单了。
马拉车(manacher) & 回文自动机(PAM)的更多相关文章
- 4.22 省选模拟赛 三元组 manacher 回文自动机
容易发现可以枚举j 那么只需要计算出 l~j这段是回文串的l的和 以及j+1~r这段是回文串的r的和. 可以manacher 之后想要求出以j为右端点的回文串左端点的和 这个东西我们通过某个点为中心的 ...
- 回文自动机pam
目的:类似回文Trie树+ac自动机,可以用来统计一些其他的回文串相关的量 复杂度:O(nlogn) https://blog.csdn.net/Lolierl/article/details/999 ...
- 回文树/回文自动机(PAM)学习笔记
回文树(也就是回文自动机)实际上是奇偶两棵树,每一个节点代表一个本质不同的回文子串(一棵树上的串长度全部是奇数,另一棵全部是偶数),原串中每一个本质不同的回文子串都在树上出现一次且仅一次. 一个节点的 ...
- 回文树(回文自动机PAM)小结
回文树学习博客:lwfcgz poursoul 边写边更新,大概会把回文树总结在一个博客里吧... 回文树的功能 假设我们有一个串S,S下标从0开始,则回文树能做到如下几点: 1.求串S前缀0~ ...
- BZOJ 3676 [Apio2014]回文串 (后缀自动机+manacher/回文自动机)
题目大意: 给你一个字符串,求其中回文子串的长度*出现次数的最大值 明明是PAM裸题我干嘛要用SAM做 回文子串有一个神奇的性质,一个字符串本质不同的回文子串个数是$O(n)$级别的 用$manach ...
- 回文自动机(PAM) 入门讲解
处理回文串,Manacher算法也是很不错,但在有些问题的处理上比较麻烦,比如求本质不同的子串的数量还需要结合后缀数组才能解决.今天的们介绍一种能够方便的解决关于回文串的问题的算法--PAM. 一些功 ...
- 洛谷P5496 回文自动机【PAM】模板
回文自动机模板 1.一个串的本质不同的回文串数量是\(O(n)\)级别的 2.回文自动机的状态数不超过串长,且状态数等于本质不同的回文串数量,除了奇偶两个根节点 3.如何统计所有回文串的数量,类似后缀 ...
- 【回文自动机】bzoj3676 [Apio2014]回文串
回文自动机讲解!http://blog.csdn.net/u013368721/article/details/42100363 pam上每个点代表本质不同的回文子串.len(i)代表长度,cnt(i ...
- 省选算法学习-回文自动机 && 回文树
前置知识 首先你得会manacher,并理解manacher为什么是对的(不用理解为什么它是$O(n)$,这个大概记住就好了,不过理解了更方便做$PAM$的题) 什么是回文自动机? 回文自动机(Pal ...
- 洛谷P4287 [SHOI2011]双倍回文(回文自动机)
传送门 听说有大佬用manacher$O(n)$过此题……太强啦…… 说一下PAM的做法吧.(看了题解之后发现)蛮简单的 我们肯定要先建出回文自动机的 然后如果是枚举每一个节点暴跳fail指针肯定得T ...
随机推荐
- 千亿参数开源大模型 BLOOM 背后的技术
假设你现在有了数据,也搞到了预算,一切就绪,准备开始训练一个大模型,一显身手了,"一朝看尽长安花"似乎近在眼前 -- 且慢!训练可不仅仅像这两个字的发音那么简单,看看 BLOOM ...
- Kafka 物理存储机制
一个商业化消息队列的性能好坏,其文件存储机制设计是衡量一个消息队列服务技术水平和最关键指标之一.下面将从 Kafka文件存储机制和物理结构角度,分析 Kafka是如何实现高效文件存储,及实际应用效果. ...
- Apollo 分布式配置中心理论到实践
携程开源的配置管理中心(统一管理各种应用配置的基础服务组件),能够集中化管理应用的不同环境,不同集群的配置,配置修改后能够实时推送到应用端,适合微服务配置管理场景.Apollo包括服务端和客户端. 在 ...
- 网络----OSI七层
OSI 订制的是一个用于计算机或通信系统间互联的标准体质(一般称为OSI参考模型或七层模型) OSI 模型把网络通信的工作分为7层分别是: 常用内容:物理层 数据链层 网络层 注解 OSI 7层 1. ...
- CSS3新增选择器(属性选择器、结构伪类选择器、伪元素选择器)
本博文介绍CSS3中新增的选择器,包括属性选择器.结构伪类选择器和伪元素选择器. 1 属性选择器 属性选择器([属性])可以根据元素的属性和属性值来对符合要求的元素进行选择. 属性选择器的基础语法如 ...
- Oracle问题:ORA-01565
问题 oracle启动时报错,找不到spfile文件. ORA-01078: failure in processing system parameters ORA-01565: error in i ...
- flask-sqlalchemy入门
Flask-SQLAlchemy 是一个为 Flask 应用增加 SQLAlchemy 支持的扩展.它致力于简化在 Flask 中 SQLAlchemy 的使用.SQLAlchemy 是目前pytho ...
- python调用打印机打印文件,图片,pdf等
引言 python连接打印机进行打印,可能根据需求的不同,使用不同的函数模块. 如果你只是简单的想打印文档,比如office文档,你可以使用ShellExecute方法,对于微软office的文档.p ...
- ASP.NET Core如何知道一个请求执行了哪些中间件?
第一步,添加Nuget包引用 需要添加两个Nuget包分别是:Microsoft.AspNetCore.MiddlewareAnalysis和Microsoft.Extensions.Diagnost ...
- idea快捷键--增强for循环
增强for循环,用于遍历:数组或单列集合 快捷键: 数组.for