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
mid∗2−i的
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字符串,如果将这个字符串0和1取反后,再将整个串反过来和原串一样,就称作“反对称”字符串。
比如00001111和010101就是反对称的,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∣,∣Y∣≥1)且
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;
}