Manacher算法模板浅析

Manacher算法总结


随便写的,个人复习用。


Manacher(马拉车)算法,主要用于处理回文串,在处理最长回文串的问题中比较有用。
比如这道模板题:洛谷P3805

入手点为已经处理过的位置,利用从而简化之后的位置。

f o r for for循环枚举最长回文串的中点 i i i ( 0 < i < n ) (0<i<n) 0<i<n
方便处理,将每两个相邻字符中间加上字符串里不会出现的字符(比如 # )。
这样,abac就会变成#a#b#a#c#。
可以忽略回文串长度奇偶的问题。

h w [ i ] hw[i] hw[i]表示以 i i i为中点的最长回文串中右半部分的长度。
e g : eg: eg#a#b#c#b#a#
c c c 的下标在处理后为 6 6 6 h w [ 6 ] = 6 hw[6]=6 hw[6]=6,那么以 c c c为中点的最长回文串长度为 h w [ 6 ] − 1 = 5 hw[6]-1=5 hw[6]1=5
理解不了就记住,其实多推几个就理解了。

那么问题就变为了求最大的 h w [ i ] − 1 hw[i]-1 hw[i]1

算法可以利用已经枚举的位置来优化,优化不了的就要暴力拓展了。
下面是优化方法:

我们在枚举的过程中记录最大的 i + h w [ i ] i+hw[i] i+hw[i],记作 m a x r i g h t maxright maxright,指的是已经枚举过的中点中,所拓展的回文串的右边最右的位置。
如果当前枚举的点 i i i m a x r i g h t maxright maxright的左边,那么就可以优化。

此时 h w [ i ] hw[i] hw[i]的最小值就是 i i i关于 m i d mid mid的对称点 m i d ∗ 2 − i mid*2-i mid2i h w hw hw。(自己画图推吧)。
然后再暴力拓展。

看起来只是很简单的优化,在实际计算中会剩下不少功夫。
以下为模板代码:

#include<bits/stdc++.h>
using namespace std;
char a[11000010],s[22000010];
int n,hw[22000010];
void change(){
	s[0]=s[1]='#';
	for(int i=0;i<n;i++){
		s[i*2+2]=a[i];
		s[i*2+3]='#';
	}
	n=n*2+2;
	s[n]=0;
}
void manacher(){
	int maxr=0,mid;
	for(int i=1;i<n;i++){
		if(i<maxr) hw[i]=min(hw[(mid<<1)-i],maxr-i-1);
		else       hw[i]=1;
		for(;s[i+hw[i]]==s[i-hw[i]];++hw[i]);
		if(i+hw[i]>maxr){
			maxr=i+hw[i];
			mid=i;
		}
	}
}
int main(){
	scanf("%s",a);
	n=strlen(a);
	change();
	manacher();
	int ans=1;
	for(int i=0;i<n;i++){
		ans=max(ans,hw[i]);
	}
	ans--;
	cout<<ans<<endl;
}

附加一道变形:
洛谷P3501
题意也粘过来吧:

对于一个01字符串,如果将这个字符串01取反后,再将整个串反过来和原串一样,就称作“反对称”字符串。
比如00001111010101就是反对称的,1001就不是。

现在给出一个长度为N的01字符串,求它有多少个子串是反对称的。

题意很简单,下面是一个简单的样例:

[input]
8
11001011
[output]
7

既然放在这里了,那这道题肯定是可以用Manacher来写的。

通过一个简单的观察,我们很清晰地可以知道,反对称只会在偶数串里出现,所以Manacher原有的变形可以忽略掉,只需要对偶数进行讨论就好了。

经过Manacher模板的学习,我们很清楚的知道,只要把里面暴力枚举时的==改为!=就行了。
然后就是一些小小的细节了,详细见代码:

//不需要考虑奇偶,反对称只能是偶数串
//hw[i]表示以i和i+1中间为反对称的最长长度,即反对称的个数
//Manacher的优化方法依然可以用
#include<bits/stdc++.h>
using namespace std;
char a[500010];
int n,hw[500010];
long long ans;
void Manacher(){
	int maxr=0,mid;
	for(int i=1;i<=n;i++){//多了个=
		if(i<maxr) hw[i]=min(hw[(mid<<1)-i],maxr-i-1);
		for(;a[i+hw[i]]!=a[i-hw[i]-1]&&i-hw[i]-1>=1&&i+hw[i]<=n;++hw[i]);//注意边界,思考为什么多了边界的判定
		if(i+hw[i]>maxr){
			maxr=i+hw[i];
			mid=i;
		}
	}
}
int main(){
	scanf("%d\n",&n);
	for(int i=1;i<=n;i++) scanf("%c",&a[i]);//如果改了很久都不对就要注意一下输入了
	Manacher();//少了一个change,让本就不长的代码显得格外的短
	for(int i=1;i<=n;i++) ans+=hw[i];//就这样求的,不想解释了
	printf("%lld",ans);//被longlong整自闭了,现在写啥都带longlong,不写估计也行
}

再来一个():

题目:
输入长度为 n n n的串 S S S,求 S S S的最长双回文子串 T T T,即可将 T T T分为两部分 X X X Y Y Y,( ∣ X ∣ , ∣ Y ∣ ≥ 1 |X|,|Y|≥1 X,Y1)且 X X X Y Y Y都是回文串。
样例:

[input]:
baacaabbacabb

[output]:
12

很容易想到把两个回文串对接起来,但是如果只通过hw来实现就。。乏善可陈。
所以来维护些别的东西吧!比如以i结尾的最长回文串长度,和以i为开头的,这样就可以轻松解决这道题了。

代码如下:

#include<bits/stdc++.h>
using namespace std;
char a[100010],s[200010];
int n,hw[200010],ll[200010],rr[200010],ans;
void change(){
	s[0]=s[1]='#';
	for(int i=0;i<n;i++){
		s[i*2+2]=a[i];
		s[i*2+3]='#';
	}
	n=n*2+2;
	s[n]=0;
}
void manacher(){
	int maxr=0,mid;
	for(int i=1;i<n;i++){
		if(i<maxr) hw[i]=min(hw[(mid<<1)-i],maxr-i-1);
		else hw[i]=1;
		for(;s[i+hw[i]]==s[i-hw[i]];++hw[i]);
		if(i+hw[i]>maxr){
			maxr=i+hw[i];
			mid=i;
		}
		ll[i+hw[i]-1]=max(ll[i+hw[i]-1],hw[i]-1);
		rr[i-hw[i]+1]=max(rr[i-hw[i]+1],hw[i]-1);
	}
}
int main(){
	scanf("%s",a);
	n=strlen(a);
	change();
	manacher();
	for(int i=1;i<=n;i+=2) rr[i]=max(rr[i],rr[i-2]-2);
	for(int i=n;i>=1;i-=2) ll[i]=max(ll[i],ll[i+2]-2);
	for(int i=1;i<=n;i+=2) if(rr[i]&&ll[i]) ans=max(ans,ll[i]+rr[i]);
	cout<<ans<<endl;
	return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值