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

Luogu P4287 [SHOI2011]双倍回文

法1

\((O(nlgn))\):
在后来的字符串中枚举中心\(i\),再枚举左中心\(j\),则有

\[ \left\{\begin{aligned} j+R[j]-1>=i\\ \frac{i+i-R[i]+1}{2}<=j \end{aligned}\right. \]

发现上一个和之前基站建设很像,对于\(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;
}
posted @ 2021-03-06 13:23  wwwsfff  阅读(81)  评论(0)    收藏  举报