Manacher例题问题汇总
Manacher例题问题汇总
本篇随笔面向个人
本来以为回文串很简单,但是没有做对应的练习前下此定论为时过早。
https://www.ybtoj.com.cn/contest/75
模板
虽然例题中也没有模板题(因为太简短了……),但是有必要预先打一遍。
步骤
-
将原字符串头部插入
$
,尾部插入@
或\0
,再将间隔中插入#
(包括$
后和@
前也要有#
);比如说如果原串为abc
,则变换为$#a#b#c#@
。 -
定义变量mx,p,r[i]。
-
遍历新字符串。首先确定当前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;
}
双倍回文
在跑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.
对称子序列
求一个只含a
和b
的字符串有多少个非连续对称子序列。
-
位置和字符都关于某条对称轴对称。
-
选取的子序列不能是连续的一段(即不能是一个子串)。
答案对\(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。