Manacher
用途
解决回文串的问题,可以求出以某个点为中心的最长回文串长度
PS:也可以用回文自动机解决,但它求出的是所有本质不同的回文串的长度,给的形式是以某个点为结尾
代码构造
#include<bits/stdc++.h>
using namespace std;
const int N=3e7+5;
int n,R[N];
char s[N];
int main() {
char ch=getchar();
s[0]='&',s[n=1]='|';
while(ch<'a'||ch>'z') ch=getchar();
while(ch>='a'&&ch<='z') {
s[++n]=ch,s[++n]='|',ch=getchar();
}
int ans=0;
for(int i=1,r=0,mid=0;i<=n;i++) {
if(i<=r) R[i]=min(R[(mid<<1)-i],r-i+1);
while(s[i-R[i]]==s[i+R[i]]) R[i]++;
if(R[i]+i>r) r=R[i]+i-1,mid=i;
ans=max(ans,R[i]);
}
printf("%d\n",ans-1);
return 0;
}
时间复杂度分析
可以发现:如果点\(i\)的while循环执行了,势必会导致\(r\)的右移,而\(r\)只能加,所以时间复杂度是\(O(n)\)
例题1:
ybtoj 不交回文串
考虑枚举断点\(i\),则需要求出以\(i\)为开头的的回文串的数量\(pre[i]\)和以\(i\)为结尾的回文串的数量\(suf[i]\),然后对\(pre\)做前缀和后求出答案
结尾符合回文自动机的业务范围,但谁让我们是正规的Manacher讲解呢
所以用Manacher处理
以\(pre\)为例,考虑对于每个中心\(i\),\([i,i+R[i]-1]\)需要加\(1\),所以用差分
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=5e5+5;
int len,n,pre[N],R[N];
char s[N],a[N];
ll suf[N];
int main() {
while(scanf("%s",s+1)!=0) {
memset(a,0,sizeof(a));
memset(R,0,sizeof(R));
len=strlen(s+1); a[n=0]='*',a[n=1]='|';
for(int i=1;i<=len;i++) {
a[++n]=s[i],a[++n]='|';
}
for(int i=1,r=0,now=0;i<=n;i++) {
if(i<=r) R[i]=min(R[now+now-i],r-i+1);
while(a[i-R[i]]==a[i+R[i]]) R[i]++;
if(i+R[i]>r) r=R[i]+i-1,now=i;
}
memset(pre,0,sizeof(pre));
memset(suf,0,sizeof(suf));
for(int i=1,l,r;i<=n;i++) {
l=(i-R[i]+1>>1)+1;
r=(i+R[i]-1>>1);
if(l<=r) {
pre[l]++,pre[(l+r+2)>>1]--;
suf[r]++,suf[(l+r-1)>>1]--;
}
}
for(int i=len-1;i;i--) {
suf[i]+=suf[i+1];
}
for(int i=2;i<=len;i++) {
suf[i]+=suf[i-1];
}
ll ans=0; ll t=pre[1];
for(int i=2;i<=len;i++) {
t+=pre[i];
ans+=t*suf[i-1];
}
printf("%lld\n",ans);
}
return 0;
}
Manacher可以很轻松地枚举本质不同的回文串
例题2
法1
\((O(nlgn))\):
在后来的字符串中枚举中心\(i\),再枚举左中心\(j\),则有
发现上一个和之前基站建设很像,对于\(i\)单调增是可以通过Vector维护
第2个式子用set维护
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5;
int n,m,R[N];
char a[N],s[N];
vector<int>V[N];
set<int>S;
int main(){
freopen("1.in","r",stdin);
scanf("%d%s",&n,s+1);
a[0]='&',a[m=1]='|';
for(int i=1;i<=n;i++) {
a[++m]=s[i],a[++m]='|';
}
for(int i=1,mid=0,r=0;i<=m;i++) {
if(i<=r) R[i]=min(R[(mid<<1)-i],r-i+1);
while(a[i+R[i]]==a[i-R[i]]) R[i]++;
if(i+R[i]>r) {
mid=i,r=i+R[i]-1;
}
}
for(int i=1;i<=m;i+=2) {
V[i+R[i]-1].push_back(i);
}
int ans=0;
for(int i=1;i<=m;i+=2) {
auto t=S.lower_bound(i+i-R[i]+1>>1);
if(t!=S.end()) {
ans=max(ans,i-(*t));
}
S.insert(i);
for(auto v:V[i]) {
S.erase(v);
}
}
printf("%d\n",ans<<1);
return 0;
}
法2:
时间复杂度\(O(n)\)
利用性质:字符串至多有\(n\)个本质不同的回文串,且如果将结尾为\(1..n\)的最长字符串取出,正好是包含了所有本质不同的回文串
(见回文自动机)
所以Manacher,发现当右端点\(r\)增加时,由于中心\(i\)时单调增的,所以此时的回文串是结尾为\(r\)的最长回文串,计算即可
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5;
int n,ans,cnt,R[N];
char s[N],a[N];
int main(){
scanf("%d%s",&n,s+1);
a[0]='*',a[cnt=1]='|';
for(int i=1;i<=n;i++) {
a[++cnt]=s[i],a[++cnt]='|';
}
for(int i=1,mid=0,r=0;i<=cnt;i++) {
if(i<r) R[i]=min(R[(mid<<1)-i],r-i+1);
while(a[i-R[i]]==a[i+R[i]]) R[i]++;
if(i+R[i]>r) {
if(i&1) {
for(int j=r+1,k;j<=i+R[i]-1;j++) {
if(j&1) {
k=(i<<1)-j;
k=(k+i)>>1;
if((k&1)&&k+R[k]-1>=i) {
ans=max(ans,(i-k)<<1);
}
}
}
}
r=i+R[i]-1; mid=i;
}
}
printf("%d\n",ans);
return 0;
}
例题3
Luogu P4555 [国家集训队]最长双回文串
回文自动机特别方便
法1:同例1
设\(pre[i]\)表示以\(i\)点为开头的最长回文串的答案
显然,随着\(i\)的单调增,对称轴可以取\(max\),即可求出\(pre[i]\)
\(suf[i]\)表示以\(i\)为结尾的答案,同理
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=5e5+5;
int len,n,pre[N],suf[N],R[N],ans;
char s[N],a[N];
int main() {
scanf("%s",s+1);
len=strlen(s+1); a[n=0]='*',a[n=1]='|';
for(int i=1;i<=len;i++) {
a[++n]=s[i],a[++n]='|';
}
for(int i=1,r=0,now=0;i<=n;i++) {
if(i<=r) R[i]=min(R[now+now-i],r-i+1);
while(a[i-R[i]]==a[i+R[i]]) R[i]++;
if(i+R[i]>r) r=R[i]+i-1,now=i;
}
for(int i=1;i<=n;i++) suf[i]=1e9;
for(int i=1,l,r;i<=n;i++) {
l=i-R[i]+1,r=i+R[i]-1;
pre[l]=max(pre[l],i);
suf[r]=min(suf[r],i);
}
for(int i=1;i<=n;i++) {
pre[i]=max(pre[i],pre[i-1]);
}
for(int i=2;i<=n;i+=2) {
pre[i>>1]=((pre[i]<<1)-i)>>1;
}
for(int i=n-1;i;i--) {
suf[i]=min(suf[i],suf[i+1]);
}
for(int i=2;i<=n;i+=2) {
suf[i>>1]=(suf[i]<<1)-i+1>>1;
}
int ans=0;
for(int i=2;i<=len;i++) {
ans=max(ans,pre[i]-suf[i-1]+1);
}
printf("%d\n",ans);
return 0;
}
法2:没有必要枚举记对称轴,长度和对称轴一一对应,所以可以直接记长度
发现每次\(i-1->i\):pre-2,所以与上一个-2取Max即可
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=5e5+5;
int len,n,pre[N],suf[N],R[N],ans;
char s[N],a[N];
int main() {
scanf("%s",s+1);
len=strlen(s+1); a[n=0]='*',a[n=1]='|';
for(int i=1;i<=len;i++) {
a[++n]=s[i],a[++n]='|';
}
for(int i=1,r=0,now=0;i<=n;i++) {
if(i<=r) R[i]=min(R[now+now-i],r-i+1);
while(a[i-R[i]]==a[i+R[i]]) R[i]++;
if(i+R[i]>r) r=R[i]+i-1,now=i;
}
for(int i=1,l,r;i<=n;i++) {
l=i-R[i]+2>>1,r=i+R[i]-1>>1;
pre[l]=max(pre[l],r-l+1);
suf[r]=max(suf[r],r-l+1);
}
for(int i=1;i<=len;i++) {
pre[i]=max(pre[i],pre[i-1]-2);
}
for(int i=len-1;i;i--) {
suf[i]=max(suf[i],suf[i+1]-2);
}
int ans=0;
for(int i=2;i<=len;i++) {
ans=max(ans,pre[i]+suf[i-1]);
}
printf("%d\n",ans);
return 0;
}

Manacher
浙公网安备 33010602011771号