ybtAu「字符串算法」第1章 Manacher

这是 neatisaac 的金牌导航题解!

为方便处理,下文代码中的字符串(如abc)都转换为形如 %#a#b#c#&的字符串,题解中不变。

A. 【例题1】不交回文串

求出每个位置的回文半径之后,利用差分求出以每个位置为开头和结尾的回文串数量,扫一遍求出两个回文串一个在 \(i\) 前面一个在 \(i\) 后面的方案数并求和。

#include <iostream>
#include <cctype>
#define N 200005
#define int long long
std::string s,S;
int r[N],pre[N],suf[N],sum[N],n,len;
void gen()
{
	S+='%';
	for(int i=0;i<n;i++) S+='#',S+=s[i];
	S+='#',S+='&';
}
void manacher()
{
	len=S.size();
	int mx=0,p=0;
	for(int i=1;i<len-1;i++)
	{
		if(i<mx) r[i]=std::min(r[2*p-i],mx-i);
		else r[i]=1;
		while(S[i-r[i]]==S[i+r[i]]) r[i]++;
		if(i+r[i]>mx) mx=i+r[i],p=i;
	}
}
signed main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	while(std::cin>>s)
	{
		S="";
		n=s.size(),gen(),manacher();
		for(int i=0;i<=n;i++) pre[i]=suf[i]=sum[i]=0;
		for(int i=2;i<=n*2;i++)
		{
			int x=i+1>>1;
			suf[x]++,suf[x+(r[i]>>1)]--;
		}
		for(int i=n*2;i>=2;i--)
		{
			int x=i>>1;
			pre[x]++,pre[x-(r[i]>>1)]--;
		}
		int ans=0;
		for(int i=n;i;i--) pre[i]+=pre[i+1];
		for(int i=1;i<=n;i++) suf[i]+=suf[i-1],sum[i]+=sum[i-1]+suf[i];
		for(int i=1;i<=n;i++) ans+=sum[i-1]*pre[i];
		std::cout<<ans<<'\n';
	}
}

B. 最长双回文串

枚举分界线 \(i\),找到以 \(i-1\) 为结尾的最长回文串和以 \(i+1\) 为开头的最长回文串,加起来更新答案。
问题变成如何求以每个位置为开头和结尾的最长回文串。
manacher 求出每个位置的回文半径之后,可以更新该位置为中心的最长回文串两边的答案。然而这还不够,因为有些回文串不是以它的中心为中心的最长回文串,要考虑这些串的答案。
发现以 \(i-1\) 为结尾的最长回文串长度减 \(1\) 可以更新 \(i\),以 \(i+1\) 为开头的最长回文串长度减 \(1\) 可以更新 \(i\)。手玩一下发现确实是这样。

#include <iostream>
#define N 200005
std::string s,S;
int p[N],n,l[N],r[N];
void manacher()
{
	int mx=0,mp=0;
	for(int i=1;i<S.size()-1;i++)
	{
		if(i<mx) p[i]=std::min(p[2*mp-i],mx-i);
		else p[i]=1;
		while(S[i-p[i]]==S[i+p[i]]) p[i]++;
		if(i+p[i]>mx) mx=i+p[i],mp=i;
	}
}
int main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	std::cin>>s,n=s.size();
	S="%#";
	for(int i=0;i<n;i++) S+=s[i],S+='#';
	S+='&';
	manacher();
	for(int i=2;i<=n*2;i++)
		l[i+p[i]-1]=std::max(l[i+p[i]-1],p[i]-1),
		r[i-p[i]+1]=std::max(r[i-p[i]]+1,p[i]-1);
	for(int i=2;i<=n*2;i++) r[i]=std::max(r[i],r[i-2]-2);
	for(int i=n*2;i>=2;i--) l[i]=std::max(l[i],l[i+2]-2);
	int ans=0;
	for(int i=2;i<=n*2;i++) ans=std::max(ans,l[i]+r[i]);
	std::cout<<ans;
}

C. 字符串连接

要求字符串最少被多少个可交回文串完全覆盖。
求出每个位置的最长回文半径之后,把该回文半径对应的回文串当成一个线段,变成了一道经典的贪心问题。
对线段按左端点排序,去掉被包含的,每次找到最后一个左端点在上一个被覆盖前的线段。

致敬传奇 CSP-S 2024 超速检测。

拼尽全力无法战胜
学生 T2 得 20 分

#include <iostream>
#include <algorithm>
#define N 100005
std::string s,S;
std::pair<int,int> a[N],b[N];
int n,p[N];
void manacher()
{
	int mx=0,mp=0;
	for(int i=0;i<S.size();i++)
	{
		if(i<mx) p[i]=std::min(p[2*mp-i],mx-i);
		else p[i]=1;
		while(S[i-p[i]]==S[i+p[i]]) p[i]++;
		if(i+p[i]>mx) mx=i+p[i],mp=i;
	}
}
int main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	while(std::cin>>s)
	{
		S="%#",n=s.size();
		for(int i=0;i<n;i++) S+=s[i],S+='#';
		S+='&',manacher();
		int cnt=0,len=0;
		for(int i=2;i<=n*2;i++) a[++cnt]={i-p[i]+1,-i-p[i]+1};
		std::sort(a+1,a+cnt+1);
		for(int i=1;i<=cnt;i++) a[i].second*=-1;
		for(int i=1;i<=cnt;i++)
		{
			if(a[i].second<=b[len].second) continue;
			b[++len]=a[i];
		}
		int la=0,ans=0;
		for(int i=1,j;i<=len;i=j+1)
		{
			j=i;
			while(j<len&&b[j+1].first<=la) j++;
			ans++,la=b[j].second;
		}
		std::cout<<ans-1<<'\n';
	}
}

D. 三个回文串

如果 \(S\) 能分成三个回文串,那么 \(S\) 一定由一个回文前缀、一个回文后缀和一个它们之间的回文串组成。
考虑枚举回文前后缀,判断所夹的串是不是回文串,然而这样是 \(O(n^2)\) 的,无法通过。于是考虑双指针优化,对每个前缀取与之不交的最长后缀判断即可。

#include <iostream>
#include <cstring>
#define N 100005
std::string s;
char S[N];
int p[N],st1[N],st2[N],tp1,tp2,n;
void manacher()
{
	int mx=0,mp=0;
	for(int i=1;i<=2*n+1;i++)
	{
		if(i<mx) p[i]=std::min(p[2*mp-i],mx-i);
		else p[i]=1;
		while(S[i-p[i]]==S[i+p[i]]) p[i]++;
		if(i+p[i]>mx) mx=i+p[i],mp=i;
	}
}
bool check()
{
	int q=1;
	for(int i=1;i<=tp1;i++)
	{
		if(!st1[i]) continue;
		if((st2[tp2]>>1)>(st1[i]>>1)+1)
		{
			int mid=st2[tp2]+st1[i]>>1;
			if(p[mid]>=(st2[tp2]-st1[i]>>1)) return 1;
		}
		while(q<=tp2&&(st2[q]>>1)<=(st1[i]>>1)+1) q++;
		if(q<=tp2)
		{
			int mid=st2[q]+st1[i]>>1;
			if(p[mid]>=(st2[q]-st1[i]>>1)) return 1;
		}
	}
	return 0;
}
int main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	int T;
	std::cin>>T;
	while(T--)
	{
		memset(p,0,sizeof p),memset(st1,0,sizeof st1),memset(st2,0,sizeof st2);
		std::cin>>s;
		n=s.size(),tp1=0,tp2=0,S[0]='%';
		for(int i=1;i<=2*n+1;i+=2) S[i]='#',S[i+1]=s[i>>1];
		manacher();
		for(int i=1;i<=2*n;i++)
		{
			if(i-p[i]==0) st1[++tp1]=p[i]-1<<1;
			if(i+p[i]-1==2*n+1)
			{
				if((i-p[i]+1)&1) st2[++tp2]=i-p[i]+2;
				else st2[++tp2]=i-p[i]+1;
			}
		}
		std::cout<<(check()?"Yes\n":"No\n");
	}
}

E. 有趣回文串

\(L_i\) 表示以 \(i\) 结束的回文串左端点和,\(R_i\) 表示以 \(i\) 开始的回文串右端点和。
对于每个位置 \(i\),其对左边回文范围内的 \(j\)\([j,2i-j]\) 是一个回文串,对 \(R_j\) 贡献为 \(2i-j\)
其对右边回文范围内的 \(j\)\([2i-j,j]\) 是一个回文串,对 \(L_j\) 贡献为 \(2i-j\)
最后枚举回文串拼接位置求解即可。代码中 \(L\)\(R\) 是反过来的。

#include <iostream>
#include <cctype>
#include <cstring>
#define N 2000005
#define mod 1000000007
#define int long long
std::string s,S;
int n,p[N],L[N],R[N],b1[N],b2[N];
void manacher()
{
	int mx=0,mp=0;
	for(int i=1;i<S.size();i++)
	{
		if(i<mx) p[i]=std::min(p[2*mp-i],mx-i);
		else p[i]=1;
		while(S[i-p[i]]==S[i+p[i]]) p[i]++;
		if(i+p[i]>mx) mx=i+p[i],mp=i;
	}
}
signed main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	while(std::cin>>s)
	{
		n=s.size(),S="%#";
		for(int i=0;i<n;i++) S+=s[i],S+='#';
		S+='&',manacher();
		memset(b1,0,sizeof b1),memset(b2,0,sizeof b2);
		for(int i=1;i<=2*n;i++) b1[i-p[i]+1]+=i,b2[i-p[i]+1]++,b1[i+1]-=i,b2[i+1]--;
		for(int i=1;i<=2*n;i++)
		{
			b1[i]+=b1[i-1],b2[i]+=b2[i-1];
			if(islower(S[i])) L[i>>1]=(b1[i]-i/2*b2[i]%mod)%mod;
		}
		memset(b1,0,sizeof b1),memset(b2,0,sizeof b2);
		for(int i=1;i<=2*n;i++) b1[i+p[i]]-=i,b2[i+p[i]]--,b1[i]+=i,b2[i]++;
		for(int i=1;i<=2*n;i++)
		{
			b1[i]+=b1[i-1],b2[i]+=b2[i-1];
			if(islower(S[i])) R[i>>1]=(b1[i]-i/2*b2[i]%mod)%mod;
		}
		int ans=0;
		for(int i=1;i<n;i++) (ans+=L[i+1]*R[i]%mod)%=mod;
		std::cout<<ans<<'\n';
	}
}

\[\Huge End \]

posted @ 2025-05-27 15:15  整齐的艾萨克  阅读(31)  评论(0)    收藏  举报