马拉车(Manacher)感性瞎扯
Manacher=马拉车
大家好,我们今天来扯Manacher算法了。
I.马拉车可以干什么?
一句话:对于一个字符串,在时间内,求出它的最长回文子串。
II.预处理
对于一个字符串,它的回文串可以有两种类型:
A.奇回文串
例:AACCBCCAA
特征:有单一回文中心(例中的B)。
B.偶回文串
例:AABBCCBBAA
特征:对称中心是两个(例中的BB)。
两者性质并不相同,必须分类讨论。
但是,我们有一种方法,可以将两者统一成一种类型:
在原串每两个字符之间,以及串头串尾,都加上一个无关字符(例如 # 等)。
例:
AABBCCDD #A#A#B#B#C#C#D#D#。
ABCBA #A#B#C#B#A#。
这样的话,原串中的奇回文串与偶回文串,都被统一成了奇回文串。(奇回文串变成以原串字符为对称中心的回文串,偶回文串变成以 # 为对称中心的回文串)。
代码(在读入时直接进行操作):
inline void getln(){
s[0]='$',S=1;
char c=getchar();
while(c>'z'||c<'a')c=getchar();
while(c>='a'&&c<='z')s[S++]=c,s[S++]='$',c=getchar();
s[S]='\0';
}
III.主算法
(默认字符串从开始)
(暂时忽略添加进来的 # 字符,因为忽略也无影响)
先考虑暴力思路:枚举对称中心,然后枚举对称半径。总复杂度。
(当然,可以哈希+二分达到,但是这个思路对我们没有帮助)
同kmp算法一样,我们可以思考一些操作来避免重复枚举。
这时候我们就可以设两个变量:
:在沿着顺序匹配时,当前所有出现过的回文串中,它的右边界达到的最右位置。
例:ABABAAC,在位置时,。在位置和的回文串都曾达到这里。
:对于当前的,它所对应的对称中心。如果有多个,取最左端的一个。
例:ABABAAC,在位置时,。在位置和的回文串都曾达到,但是是最左端的一个。
我们再设一个数组,表示位置时的回文半径。
字母 | A | B | A | B | A | A | C |
---|---|---|---|---|---|---|---|
1 | 2 | 3 | 2 | 1 | 1 | 1 |
(自动忽略了偶回文串)。
接下来我们就开始操作了。
初始令。
对于当前位置:
1.
暴力延伸,这里是未涉及到的新地方。
2.
这时,我们令为关于的对称点(即)。
同时令为关于的对称点。
再令为以为对称中心的回文串的左边界。
2.1.
因为是回文串,所以有
则关于对称的回文串,也是关于对称的回文串(想一想,左右对称)。
如图:
2.2.
对称的只有从到的一段,剩下的部分两边是不同的,仍需暴力扩展。
如图:
IV.实现
inline void Manacher(){
Centre=Rpos=-1;
for(register int i=0;i<S;i++){
rad[i]=(i<Rpos)?min(Rpos-i,rad[Centre*2-i]):1;
while(i+rad[i]<S&&i-rad[i]>=0)if(s[i+rad[i]]==s[i-rad[i]])rad[i]++;else break;
if(i+rad[i]>Rpos)Rpos=i+rad[i],Centre=i;
Ans=max(Ans,rad[i]);
}
}
实现与上面的描述很不一致,我们逐行分析。
rad[i]=(i<Rpos)?min(Rpos-i,rad[Centre*2-i]):1;
用了三目运算符。
它的意思是:
如果,那么
回忆一下,就是。
,就是上文中的可用部分。
,就是上文中的可用部分。
两者取,就很显然了。
而当时,(默认单个字符本身就为回文串)。
然后就是暴力跳了。
while(i+rad[i]<S&&i-rad[i]>=0)if(s[i+rad[i]]==s[i-rad[i]])rad[i]++;else break;
分析一下,在情形和,都需要暴力跳。在情形,暴力跳一次就会退出。
然后按定义更新和,同时取答案。
if(i+rad[i]>Rpos)Rpos=i+rad[i],Centre=i;
Ans=max(Ans,rad[i]);
最终答案为,因为在长度为的原串中加入了个 # 。但是又是半径,也要再加入个字符才能构成回文串。
总复杂度。分析复杂度是不可能的,这辈子都是不可能的。
V.大礼包
本文所有代码使用的分隔符都是dollar符号,但是由于的缘故,在讲解时使用 # 符号。另,还是因为,文中的dollar符号全部莫名其妙多了一个空格,请自行删除。
(代码:【模板】manacher算法
#pragma GCC optimise(3)
#include<bits/stdc++.h>
using namespace std;
char s[22000100];
int S,rad[22001000],Centre,Rpos,Ans;
inline void getln(){
s[0]='$',S=1;
char c=getchar();
while(c>'z'||c<'a')c=getchar();
while(c>='a'&&c<='z')s[S++]=c,s[S++]='$',c=getchar();
s[S]='\0';
}
inline void Manacher(){
Centre=Rpos=-1;
for(register int i=0;i<S;i++){
rad[i]=(i<Rpos)?min(Rpos-i,rad[Centre*2-i]):1;
while(i+rad[i]<S&&i-rad[i]>=0)if(s[i+rad[i]]==s[i-rad[i]])rad[i]++;else break;
if(i+rad[i]>Rpos)Rpos=i+rad[i],Centre=i;
Ans=max(Ans,rad[i]);
}
}
int main(){
getln();
Manacher();
printf("%d\n",Ans-1);
return 0;
}
(忽略上面的O3)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?