马拉车(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 ...
随机推荐
- Python 3.11.官方文档
索引 模块 | Python » English Spanish French Japanese Korean Brazilian Portuguese Simplified Chinese Trad ...
- [Linux]Linux发展历程
古人云,知其然知其所以然.马哲思想指导着我们,任何事物.问题,离不开:为什么(Why,事物从哪里来?).是什么(What,事物的定位?).怎么做(How,到哪里去?)的哲学3问. 继上个月算是相对彻底 ...
- 【Diary】CSP-J 2019 游记
大废话游记. CSP-J1 Day-1 写13年的初赛题.感觉挺简单.但是问题求解第二问数数数错了,加上各种sb错误,只写了八十几分... 然后跑去机房问,那个相同球放相同袋子的题有没有数学做法. 没 ...
- LeeCode 91双周赛复盘
T1: 不同的平均值数目 思路:排序 + 双指针 + 哈希存储 public int distinctAverages(int[] nums) { Arrays.sort(nums); Set< ...
- 运行项目报错Cannot read property 'styles' of undefined
原因是安装依赖版本不对,以下是我的解决办法: 1.先删除项目中package-lock.json 文件 及node_modules文件(可使用rimraf指令删除node_modules,直接删文件很 ...
- .NET敏捷开发框架-RDIFramework.NET V5.1发布(跨平台)
RDIFramework.NET,基于全新.NET Framework与.NET Core的快速信息化系统敏捷开发.整合框架,给用户和开发者最佳的.Net框架部署方案.为企业快速构建跨平台.企业级的应 ...
- C# 反射 操作列表类型属性
本文介绍对列表进行创建及赋值的反射操作 我们现在有TestA.TestB类,TestA中有TestB类型列表的属性List,如下: 1 public class TestA 2 { 3 public ...
- VS2022使用ClickOnce发布程序本地安装.net框架
因为遇到下面的错误,没有在网上搜到详细解决问题的教程,费了一些时间才解决了问题,特此记录一下,也希望能帮助到其他人. 要在"系统必备"对话框中启用"从与我的应用程序相同的 ...
- 数据结构与算法大作业:走迷宫程序(C语言,DFS)(代码以及思路)
好家伙,写大作业,本篇为代码的思路讲解 1.大作业要求 走迷宫程序 问题描述: 以一个 m * n 的长方阵表示迷宫, 0和1分别表示迷宫的通路和障碍. 设计一个程序, 对任意设定的迷宫, 求出一 ...
- drf重写authenticate方法实现多条件登录(源码分析)
drf重写authenticate方法实现多条件登录(源码分析) 1. 思路 JWT拓展的登录视图中, 在接受到用户名和密码时, 调用的也是Django的认证系统中提供的authenticate()来 ...