Manacher例题问题汇总

Manacher例题问题汇总

本篇随笔面向个人

本来以为回文串很简单,但是没有做对应的练习前下此定论为时过早。

https://www.ybtoj.com.cn/contest/75

模板

虽然例题中也没有模板题(因为太简短了……),但是有必要预先打一遍。

步骤

  1. 将原字符串头部插入$,尾部插入@\0,再将间隔中插入#(包括$后和@前也要有#);比如说如果原串为abc,则变换为$#a#b#c#@

  2. 定义变量mx,p,r[i]。

  3. 遍历新字符串。首先确定当前r[i]的下界为\(i<mx?\min(r[2p-i],mx-i):1\),接着尝试暴力向两边拓展,最后看看能不能更新\(mx\),若能则同时更新\(p\)

代码

void manacher()
{
	int mx=1,p=1;
	for(int i=1;i<str.size()-1;i++)
	{
		r[i]=i<mx?min(mx-i,r[2*p-i]):1;
		while(str[i-r[i]]==str[i+r[i]]) r[i]++;
		if(mx<i+r[i])
		{
			p=i;
			mx=i+r[i];
		}
	}
}

注意事项

不建议用string(虽然上面的代码的确是string)

注意\(\min\)中的\(r[2*p-i]\),不是\(2*p-1\),同时记得下界是由对称位置的r决定而不是下标决定。

为什么是\(2*p-i\)?设\(i,j\)关于\(p\)对称,则\(\frac{i+j}{2}=p\),所以\(j=2*p-i\)

优化/简化

预处理可以用一句话解决

s[0]='$';scanf("%d %s",&n,s+1);for(int i=n;i>=1;i--) s[2*i]=s[i],s[2*i+1]='#';s[1]='#';

这同时也说明了char[]相对于string的优点之一。

例题

【例题1】不交回文串

给一个字符串\(S\),问其有多少对不相交的回文子串。

先跑模板,计算每个位置上最长回文串。再差分转前缀和地求以i开头/结尾的回文串个数,再对其中一个做前缀和,用另一个乘一下即为答案。

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<map>
#include<set>
#include<queue>
#include<vector>
#define IL inline
#define re register
#define LL long long
#define ULL unsigned long long
#ifdef TH
#define debug printf("Now is %d\n",__LINE__);
#else
#define debug
#endif
using namespace std;

template<class T>inline void read(T&x)
{
	char ch=getchar();
	int fu;
	while(!isdigit(ch)&&ch!='-') ch=getchar();
	if(ch=='-') fu=-1,ch=getchar();
	x=ch-'0';ch=getchar();
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	x*=fu;
}
inline int read()
{
	int x=0,fu=1;
	char ch=getchar();
	while(!isdigit(ch)&&ch!='-') ch=getchar();
	if(ch=='-') fu=-1,ch=getchar();
	x=ch-'0';ch=getchar();
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	return x*fu;
}
int G[55];
template<class T>inline void write(T x)
{
	int g=0;
	if(x<0) x=-x,putchar('-');
	do{G[++g]=x%10;x/=10;}while(x);
	for(int i=g;i>=1;--i)putchar('0'+G[i]);putchar('\n');
}
string str,t;
LL r[300010],pre[300010],suf[300010],sum[300010],ans;
void init()
{
	memset(r,0,sizeof(r));
	memset(pre,0,sizeof(pre));
	memset(suf,0,sizeof(suf));
	memset(sum,0,sizeof(sum));
	t.clear();
	t="$#";
	for(int i=0;i<str.size();i++)
	{
		t+=str[i];
		t+='#';
	}
	t+='@';
	str=t;
}
void manacher()
{
	int mx=1,p=1;
	for(int i=1;i<str.size()-1;i++)
	{
		if(i<mx) r[i]=min((LL)mx-i,r[2*p-i]);
		else r[i]=1;
		while(str[i-r[i]]==str[i+r[i]]) r[i]++;
		if(mx<i+r[i])
		{
			p=i;
			mx=i+r[i];
		}
	}
}
int main()
{
	while(cin>>str)
	{
		int len=str.size();
		init();
//		cout<<str<<endl;
		manacher();
		for(int i=2;i<=len*2;i++)
		{
			int x=(i+1)/2;
			suf[x]++;
			suf[x+r[i]/2]--;
		}
		for(int i=len*2;i>=2;i--)
		{
			int x=i/2;
			pre[x]++;
			pre[x-r[i]/2]--;
		}
		for(int i=len;i>=1;i--)
		{
			pre[i]+=pre[i+1];
		}
		for(int i=1;i<=len;i++)
		{
			suf[i]+=suf[i-1];
			sum[i]=sum[i-1]+suf[i];
		}
		ans=0;
		for(int i=1;i<=len;i++)
		{
			ans+=pre[i]*sum[i-1];
		}
		cout<<ans<<endl;
	}
	return 0;
}

双倍回文

题面亦可见洛谷 P4287 [SHOI2011]双倍回文

在跑manacher的更新答案时,同时看看当前回文串的前一部分的后缀是否也是一个回文串。此判定可以用之前计算过的\(r[i]\)判定。

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<map>
#include<set>
#include<queue>
#include<vector>
#define IL inline
#define re register
#define LL long long
#define ULL unsigned long long
#ifdef TH
#define debug printf("Now is %d\n",__LINE__);
#else
#define debug
#endif
using namespace std;

template<class T>inline void read(T&x)
{
	char ch=getchar();
	int fu;
	while(!isdigit(ch)&&ch!='-') ch=getchar();
	if(ch=='-') fu=-1,ch=getchar();
	x=ch-'0';ch=getchar();
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	x*=fu;
}
inline int read()
{
	int x=0,fu=1;
	char ch=getchar();
	while(!isdigit(ch)&&ch!='-') ch=getchar();
	if(ch=='-') fu=-1,ch=getchar();
	x=ch-'0';ch=getchar();
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	return x*fu;
}
int G[55];
template<class T>inline void write(T x)
{
	int g=0;
	if(x<0) x=-x,putchar('-');
	do{G[++g]=x%10;x/=10;}while(x);
	for(int i=g;i>=1;--i)putchar('0'+G[i]);putchar('\n');
}
int n;
int len;
char ch[1000010];
string t;
int r[1000010];
int p=0,mx=0,ans;
int main()
{
	n=read();
	cin>>t;
	ch[len++]='$';
	ch[len++]='#';
	for(int i=0;i<t.size();i++)
	{
		ch[len++]=t[i];
		ch[len++]='#';
	}
	ch[len]='\0';
//	for(int i=0;i<=len;i++) cout<<ch[i];
	for(int i=1;i<=len;i++)
	{
		r[i]=i<mx?min(mx-i,r[2*p-i]):1;
		while(ch[i-r[i]]==ch[i+r[i]]) r[i]++;
		if(i+r[i]>mx)
		{
			if(i&1)
			{
				for(int j=max(mx,i+4);j<i+r[i];j++)
				{
					if((j-i)%4==0&&r[(i-(j-i)/2)]>(j-i)/2) ans=max(ans,j-i);
				}
			}
			mx=i+r[i];
			p=i;
		}
	}
	write(ans);
	return 0;
}

在题解中也遇到了说可以用并查集,回文自动机(PAM)的,日后了解。

最长双回文串

很容易想到Manacher,先打一个板子。处理完以i为中心的最长回文串之后就不知道该做什么了。

想起例题,在维护\(r[i]\)的同时再维护\(suf[i]\)\(pre[i]\)……仿佛这里有点意思。

在计算\(r[i]\)时同时维护\(ll[i],rr[i]\),分别表示以i为终点/起点的最长回文子串。

(然后其实这里可以用线段树维护,但会多一个\(\log\)||\(10^5\)谁会管多不多个\(\log\)呢||算了考虑各方面还是用Manacher吧)

首先在计算每一个\(r[i]\)时,只更新最长子序列的左右端点,即\(ll[i+r[i]-1],rr[i-r[i]+1]\)

注意到在原串中,相邻的回文子串,将变成,变换后的串中,以#为间隔的回文子串。

之后再扫一遍,看能否更新周围的\(ll\)\(rr\)

注意,这个更新是单向的。比如说\(rr[i]\)表示的是以\(i\)起点的最长回文子串长度,那么就只能更新它右边的\(rr[i+2]\)\(ll[i]\)同理。

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<map>
#include<set>
#include<queue>
#include<vector>
#define IL inline
#define re register
#define LL long long
#define ULL unsigned long long
#ifdef TH
#define debug printf("Now is %d\n",__LINE__);
#else
#define debug
#endif
using namespace std;

template<class T>inline void read(T&x)
{
	char ch=getchar();
	int fu;
	while(!isdigit(ch)&&ch!='-') ch=getchar();
	if(ch=='-') fu=-1,ch=getchar();
	x=ch-'0';ch=getchar();
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	x*=fu;
}
inline int read()
{
	int x=0,fu=1;
	char ch=getchar();
	while(!isdigit(ch)&&ch!='-') ch=getchar();
	if(ch=='-') fu=-1,ch=getchar();
	x=ch-'0';ch=getchar();
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	return x*fu;
}
int G[55];
template<class T>inline void write(T x)
{
	int g=0;
	if(x<0) x=-x,putchar('-');
	do{G[++g]=x%10;x/=10;}while(x);
	for(int i=g;i>=1;--i)putchar('0'+G[i]);putchar('\n');
}
int n;
char ch[500010];
int r[500010],ll[500010],rr[500010];
int main()
{
	ch[0]='$';
	scanf("%s",ch+1);
	n=strlen(ch+1);
//	cout<<n<<endl;
	for(int i=n;i>=1;i--)
	{
		ch[i*2]=ch[i];
		ch[i*2+1]='#';
	}
	ch[1]='#';
	ch[2*n+2]='@';
	for(int i=1,mx=1,p=1;i<=2*n+1;i++)
	{
		r[i]=i<mx?min(mx-i,r[2*p-i]):1;
		while(ch[i+r[i]]==ch[i-r[i]]) r[i]++;
		if(mx<i+r[i])
		{
			mx=i+r[i];
			p=i;
		}
		ll[i+r[i]-1]=max(ll[i+r[i]-1],r[i]-1);
		rr[i-r[i]+1]=max(rr[i-r[i]+1],r[i]-1);
	}
	for(int i=1;i<=2*n+1;i+=2)
	{
		rr[i+2]=max(rr[i+2],rr[i]-2);
	}
	for(int i=2*n+1;i>=3;i-=2)
	{
		ll[i-2]=max(ll[i-2],ll[i]-2);
	}
	int ans=0;
	for(int i=1;i<=2*n+1;i+=2)
	{
		if(ll[i]&&rr[i]) ans=max(ans,ll[i]+rr[i]);
	}
	write(ans);
	return 0;
}

注意第88行(倒数第5行)必须先判断ll和rr都有值,才能更新ans。

hack数据类似于awa,错误答案3,正确输出2。因为程序在计算第一个#(以及最后一个#)时,\(ll=3,rr=0\)。这种只有一边有回文串的端点不应被更新答案。

在luogu被hack,但是这里没有awa.

对称子序列

求一个只含ab的字符串有多少个非连续对称子序列

  1. 位置和字符都关于某条对称轴对称。

  2. 选取的子序列不能是连续的一段(即不能是一个子串)。

答案对\(10^9+7\)取模。

\(|S|\le 10^5\)

首先明确一个公式,非连续对称子序列数=回文子序列数-回文子串数

回文子串数可以用Manacher求出。

考虑怎么求回文子序列数?

注意到用Manacher求出的是以每条对称轴所构成的最长回文子串的长度,其实是计算了每个对称轴对答案的贡献。

那么回文子序列数也可以用类似的思想。

\(f[(i+j)/2]\)是以\(i,j\)中间点为对称轴,有多少对称的字符。

这个可以用\(FFT\)求。

类似于Manacher,可以通过插#\((i+j)/2\)变成\(i+j\)

那么这个对称轴受到的贡献就是\(2^{f[i]}-1\)。(每一对对称的字符都可以有选和不选两种情况,但是唯独有一种——一对都不选——不可以,所以要减去1)。

总的来说就是两遍\(FFT\)+快速幂+Manacher

posted @ 2021-02-15 15:33  Vanilla_chan  阅读(253)  评论(0编辑  收藏  举报