【字符串匹配】【回文】Manacher

【字符串匹配】【回文】Manacher

给定一个字符串S,求S中最长的回文子串长度。时空复杂度要求为线性。

暴力扫描T走不谢。

下面介绍马拉车Manacher算法。

首先对于任意字符串S,定义Sij表示下标从ij的连续子串。

考虑一个回文串A,如果他的长度为奇数2k+1(kN),那么就会有A1kAk+22k+1是关于Ak+1这一个字母对称的,也就是说A1k在翻转之后和Ak+22k+1完全相等。

那么就称A是以Ak+1为中心的(下文简记为以k+1为中心),半径为k+1的回文串。(这里“半径”只是我口胡的一个词,实际上应该没有这个定义。)

可以发现,这个回文串一定是奇数回文,整个回文串的长度就是两倍的半径减一。

那么就先解决奇数回文的情况。

对于一个字符串S1n来说,他可以有许多子串都是奇数回文,也可以有一些奇数回文的不同子串拥有相同的中心。由于我们只关注最长回文子串的长度,因此对于相同中心的子串我们只需要知道其中半径最长的就可以了。

所以我们定义一个序列m,其中mi表示字符串S的子串中,以i为中心的奇数回文子串的最大半径。保存半径的原因是方便转移,如果不纠结常数问题也可以直接保存长度。

明显,mi1(1in),一个字母的时候是一个奇数回文,半径为1

现在我们内心涌出了一个邪恶的想法,就是对于每一个位置,枚举以其为中心的最大半径,大概就是这样写(忽略边界问题):

m[i]=1;
while(s[i+m[i]]==s[i-m[i]]) ++m[i];

然后很快就会发现这样的转移其实也是二次方级别的,达不到线性。

考虑如何线性转移。在kmp的时候为了求出新前缀中最长公共前缀后缀的长度,我们运用了已知前缀的信息。这里我们也可以这样干。

假设当前转移到序列第i项,维护一个x(x<i),使得x+mx最大,记最大值为y

为什么要维护x+mx最大?因为对于每一个xmx表示以x为中心的最大半径,也就是说 Sxmx+1x+mx1 一定是一个奇数回文子串,而且是以x为中心的最长的奇数回文子串。如果当前位置i在某个回文子串中,而且保证了该回文子串的中心xi前面,我们就可以利用2xi位置的信息来加快i位置的求值。

如果i被包含在这个回文子串中,也就是iy,那么2xi也包含在其中(废话),而这个奇数回文子串是以x为中心的,如果记以2xi为中心,半径为(2xi)(xmx+1)+1的子串(不一定是回文串)为A,记以i为中心,半径与A相同的子串为B的话,那么就可以说,A,B是关于Sx对称的(因为A,B都被包含在以x为中心的回文串中)。

那么,就有了一个很天真的想法,既然他们对称了,如果只关心回文串的长度可以认为他们俩包含的信息相同。因此就可以说mi=m2xi

为什么这样做是不对的呢?因为子串S2xim2xi+12xi+m2xi1,也就是以2xi为中心的最大半径子串有可能已经超出了子串Sxmx+1x+mx1的范围。一旦超出这个范围,子串A,B就不再相等了(因为mx是以x为中心的最大半径,因此SxmxSx+mx

所以我们在优化的时候要取一个最小值避免超出范围。

在优化之后就是上面枚举最大半径的方法啦。

while(s[i+m[i]]==s[i-m[i]]) ++m[i]

为什么这样时间复杂度是线性的呢?

对于当前要求的mi,如果求出来发现i+mi仍然没有达到y,即这个新的奇数回文子串仍然被包含在上面维护的最大的x+mx的回文子串中,那么其实在求他的过程就是直接执行了mi=m2xi,根本没有执行过++m[i]。这是因为i+mi没有达到y,那么在对称的另一边也没有达到xmx,一切还在掌握之中,因为m2xi是最大半径,因此就不可能执行++m[i]的拓展了。

如果初始时i就达到或者超过了y,或者求出来i+mi达到或者超过了y,那么就说明他一定执行了++m[i],因为仅凭mi=m2xi是不可能做到出来的结果能达到y,我们在赋值的时候为了不让他越界而取了一个限制,因此超过就肯定是执行了++m[i]。又因为他能超过,因此在赋值的时候就应该是被上界限制住了,所以在执行++m[i]之前应该有i+mi=y=x+mx。执行++m[i]之后,执行了多少次y就会往后移动多少位,因为执行完之后要更新维护y的值。

综上所述,y的移动和++m[i]的操作是同步的,而y的最大值只能去到n,所以++m[i]最多也就只能执行n次,因此时间复杂度就是线性的了。


上面说了那么多,都是针对奇数回文子串的,但是现实中是有偶数回文的啊?怎么搞?难道把他删掉一个字符或者增加一个字符变成奇数?那不就改变字符串本来结构了吗?

其实,就是把他变成奇数,但是为了不改变原来的结构,我们需要加一堆字符。

比如字符串abccbab,他的最长奇数回文子串为bab,最长偶数回文子串为abccba,而偶数回文子串按照上面的方法是不能正确识别的。

因此我们插入一些字符,把原字符串变为#a#b#c#c#b#a#b,其实就是在原来每个字符前插入一个未曾出现过的字符。char类型是八位有符号整数,因此实际操作中建议选择一个比较大的负数插入避免和原字符串重合。这里为了方便演示而用了可见字符。

那么现在新字符串的最长奇数回文子串就变成了#a#b#c#c#b#a#,对应原串的偶数回文子串,这样就把偶数子串转化为了奇数子串,而原来的奇数子串仍然可以成功识别,他就是b#a#b

模拟可得,新字符串中mi(回文串的半径)的最大值减一就是原字符串中最长的回文串的长度。

那么现在得到转换的规则(初稿):在每一个字母前面加上一个未曾出现过的字符。

下面这个例子:aa,转换之后变成#a#a,最长奇数回文子串#a#,貌似和原串不太一样。

那么改一下转换规则(不改版):每个字母前面加上一个未曾出现过的字符,最后再添加一个这个字符。

然后现在想一下边界问题,在进行s[i-m[i]]==s[i+m[i]]拓展时有可能出现i-m[i]为负数的情况,这样会因为神秘的东方力量而导致RWA,因此我们就要保证到达s[0]的时候就不能让他继续拓展了,这个时候我们可以再次修改规则(打死不改版):

在原字符串的每个空隙中插入一个未出现过的字符,然后在新字符串的两端加入两个不同的未出现过的字符

其中的空隙表示两个字符之间或者首字母前或者尾字母后。

由于字符串结尾一般使用\0表示,他的ASCII0,因此我们只需要在开头插入另一个未出现过的字符就可以了。

Code

题目

#include <cstdio>
#include <cstring>

inline int min(const int a,const int b) { return a<b? a:b; }
inline int max(const int a,const int b) { return a>b? a:b; }
const int N=1.1e7+3;
char s[N],S[N<<1];
int m[N<<1],n,len;

int main() {
	scanf("%s",s);
	len=strlen(s);
	S[n++]=-17;
	for(int i=0;i<len;++i) {
		S[n++]=-18;
		S[n++]=s[i];
	}
	S[n++]=-18;
	S[n]=0;
	register int x=0,y=0;
	for(int i=1;i<=n;++i) {
		if(i<y) m[i]=min(m[(x<<1)-i],y-i);
		else m[i]=1;
		while(S[i-m[i]]==S[i+m[i]]) ++m[i];
		if(i+m[i]>y) y=i+m[x=i];
	}
	x=0;
	for(int i=1;i<=n;++i) x=max(x,m[i]);
	printf("%d\n",x-1);
	return 0;
}
posted @   IdanSuce  阅读(72)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示