文章目录
Manacher,又称马拉车算法,是解决字符串中回文的意大利器
本文主要是算法介绍,再加上几道例题
算法介绍
首先,为了不进行关于奇回文和偶回文的考虑,我们可以在原串中每个字母间加入一个特殊字符,比如#
例如babaa变成了#b#a#b#a#a#
我们惊奇地发现,原本的回文串bab变成了#b#a#b#,aa变成了#a#a#都是形式上的奇回文串了
然后就是算法流程了。
我们定义p[i]为以i为对称中心的最长的回文串的一半(上取整)
当前,我们最靠右的位置为mx,其所对应的回文中心为id
那么,比如我们当前处理到了i,就有这样几种情况
1. i<mx
我们考虑i关于id的对称点i’,那么,根据回文,有p[i]=min(p[i’],mx-i+1)
2.i>=mx
那么此时p[i]初值我们设为1
然后呢,我们就暴力扩展,最后再更新mx和id
时间复杂度?
考虑到如果暴力扩展每成功一次,mx就会升高一,那么暴力扩展次数就不会超过O(N)了
所以时间复杂度O(N)
代码参见例题1
例题
例题1 HDU 3068 最长回文
题意
多组数据,每次给出一个字符串S,求S的最长回文子串
|S|<=110000
题解
直接上Manacher
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
typedef long long ll;
const int N=200000+5;
const int INF=0x3f3f3f3f;
char s[N];
char t[N];
int p[N];
int n,m;
void Get_t(char *s,char *t){
n=strlen(s+1);
m=n*2+1;
t[0]=2;//!
for(int i=1;i<=n;i++){
t[i*2-1]=1;
t[i*2]=s[i];
}
t[m]=1;
}
void Manacher(char *t){
int mx=0,id=0;
for(int i=1;i<=m;i++){
if(mx>i)
p[i]=min(p[id*2-i],mx-i+1);
else
p[i]=1;
while(t[i+p[i]]==t[i-p[i]])
p[i]++;
if(p[i]+i-1>mx){
mx=p[i]+i-1;
id=i;
}
}
}
int main()
{
while(~scanf("%s",s+1)){
Get_t(s,t);
//puts(t+1);
Manacher(t);
int ans=1;
for(int i=1;i<=m;i++)
ans=max(ans,p[i]-1);
printf("%d\n",ans);
}
}
例题2 bzoj2565 最长双回文串
题意
定义双回文串为一个串可以被分割成两部分,这两个部分都非空且为回文串
现给出一个串S,问你它的最长回文子串
题解
其实呢,我们只需要知道对于一个点而言,在它左右的最远的对称中心是谁,这两个坐标相减就是答案
至于这个怎么求?可以用两次单调队列(emm…这个单调队列是按位置单调…所以完全不用弹队尾)来实现。
成功打飚一个+让我以为时间复杂度分析错了…
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
typedef long long ll;
const int N=400000+5;
const int INF=0x3f3f3f3f;
char s[N],t[N];
int p[N];
int n,m;
void Get_t(char *s,char *t){
n=strlen(s+1);
m=n*2+1;
t[0]=2;
for(int i=1;i<=n;i++){
t[i*2-1]=1;
t[i*2]=s[i];
}
t[m]=1;
}
void Manacher(char *t){
int mx=0,id=0;
for(int i=1;i<=m;i++){
if(i<mx)
p[i]=min(p[id*2-i],mx-i+1);
else
p[i]=1;
while(i-p[i]>=1&&i+p[i]<=m&&t[i+p[i]]==t[i-p[i]])
p[i]++;
if(i+p[i]-1>mx){
mx=i+p[i]-1;
id=i;
}
}
}
int L[N],R[N];
int q[N],head,tail;
int main()
{
while(~scanf("%s",s+1)){
Get_t(s,t);
//puts(t);
Manacher(t);
head=1,tail=0;
for(int i=1;i<=m;i++){
while(head<=tail&&q[head]+p[q[head]]-1<i)
head++;
q[++tail]=i;
L[i]=q[head];
}
head=1,tail=0;
for(int i=m;i>=1;i--){
while(head<=tail&&q[head]-p[q[head]]+1>i)
head++;
q[++tail]=i;
R[i]=q[head];
}
int ans=0;
for(int i=2;i<m;i++)
ans=max(ans,R[i]-L[i]);
printf("%d\n",ans);
}
}
例题3 Codeforces 17E Palisection
题意
给出一个字符串,问你有多少对回文子串有重叠(可以包含)
题解
考虑反面。
也就是说,我们先求出来总数,再减去不重叠的
具体怎么做呢?
首先我们要明确一件事,在我们加入了特殊字符后,回文串的两端都必须是这个特殊字符。也就是说[l,r]为回文串,是意味着[l+2,r-2]为回文串而非[l+1,r-1],不然的话在计算中就会出现很大的重复!
当然,还有一种做法就是干脆在马拉车之后就还原到原字符串上操作
我们定义L[i]为以i为左端点的回文串的数量,R[i]为右端点,g[i]为R[i]的前缀和
L[i]怎么求呢?我们可以差分。比如处理到以i为中心的回文子串时,让他的左端点+1,中间-1,就可以让他的左半部分数量都+1了(此处注意一下中间为#的话,中间的贡献也是不能算上的)。然后差分数组求个前缀和就是了
R[i]同理,只不过是差分数组的后缀和
现在想想,前缀和也没啥毛病
那么答案就是枚举每个(除端点)#隔开,左右两边独立,然后就用g[i]·L[i]就好了
注意不能用f[i]·g[i],否则也会有很大的重复
所以f其实没啥用…
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
typedef long long ll;
const int N=(2e6+5)*2;
const int INF=0x3f3f3f3f;
const int mod=51123987;
char s[N],t[N];
int p[N];
int n,m;
int L[N],R[N];
int f[N],g[N];
void Get_t(char *s,char *t){
n=strlen(s+1);
m=n*2+1;
t[0]=2;
for(int i=1;i<=n;i++){
t[i*2-1]=1;
t[i*2]=s[i];
}
t[m]=1;
}
void Manacher(char *t){
int mx=0,id=0;
for(int i=1;i<=m;i++){
if(i<mx)
p[i]=min(p[id*2-i],mx-i+1);
else
p[i]=1;
while(i-p[i]>=1&&i+p[i]<=m&&t[i+p[i]]==t[i-p[i]])
p[i]++;
if(i+p[i]-1>mx){
mx=i+p[i]-1;
id=i;
}
}
}
int main()
{
scanf("%*d");
while(~scanf("%s",s+1)){
Get_t(s,t);
//puts(t);
Manacher(t);
for(int i=2;i<m;i++){
L[i-p[i]+1]++;
if(i&1)
L[i]--;
else
L[i+1]--;
}
for(int i=3;i<=m;i+=2)
L[i]+=L[i-2];
for(int i=m;i>=1;i--){
f[i]=(f[i+1]+L[i])%mod;
}
for(int i=2;i<m;i++){
R[i+p[i]-1]++;
if(i&1)
R[i]--;
else
R[i-1]--;
}
for(int i=m;i>=1;i-=2)
R[i]+=R[i+2];
for(int i=1;i<=m;i++){
g[i]=(g[i-1]+R[i])%mod;
}
int ans=1ll*f[1]*(f[1]-1)/2%mod;
for(int i=3;i<m;i+=2){
ans=(ans-1ll*g[i]*L[i]%mod+mod)%mod;
}
printf("%d\n",ans);
}
}
例题4 The Number of Palindromes
题意
多组数据。
给出一个字符串,问你有多少个本质不同的回文子串
题解
额…非常暴力
直接每次进while的时候,(注意,必须当前端点是特殊符号时,否则会重复)hash之后放进set即可
然而一不小心写了unsigned的我…WA到飞起…
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<set>
#include<string>
using namespace std;
typedef long long ll;
const int N=1000005*2;
const ll mod1=1e9+7;
const ll mod2=19260817;
const ll chr=131;
int n,m;
char s[N];
int t[N];
int p[N];
typedef pair<ll,ll> pll;
set<pll>S;
ll pw1[N],pw2[N];
ll s1[N],s2[N];
pll Get_hash(int l,int r){
ll A=0,B=0;
A=((s1[r]-s1[l-1]*pw1[r-l+1]%mod1)%mod1+mod1)%mod1;
B=((s2[r]-s2[l-1]*pw2[r-l+1]%mod2)%mod2+mod2)%mod2;
return pll(A,B);
}
void Manacher(){
S.clear();
int mx=0,id=0;
for(int i=1;i<=m;i++){
if(i<mx)
p[i]=min(mx-i+1,p[id*2-i]);
else
p[i]=1;
while(i-p[i]>=1&&t[i-p[i]]==t[i+p[i]]){
p[i]++;
if((i+p[i]-1)&1){
//pll ZZZ=Get_hash(i-p[i]+1,i+p[i]-1);
S.insert(Get_hash(i-p[i]+1,i+p[i]-1));
//printf("%d\n",S.size());
}
}
if(p[i]+i-1>mx){
mx=p[i]+i-1;
id=i;
}
}
}
int main()
{
int Kase;
scanf("%d",&Kase);
pw1[0]=pw2[0]=1;
for(int i=1;i<N;i++)
pw1[i]=pw1[i-1]*chr%mod1,
pw2[i]=pw2[i-1]*chr%mod2;
for(int kase=1;kase<=Kase;kase++){
memset(s,0,sizeof s);
memset(t,0,sizeof t);
memset(p,0,sizeof p);
memset(s1,0,sizeof s1);
memset(s2,0,sizeof s2);
S.clear();
scanf("%s",s+1);
n=strlen(s+1);
m=2*n+1;
t[0]=28;
for(int i=1;i<=n;i++)
t[i*2-1]=27,t[i*2]=s[i]-'a'+1;
t[m]=27;
s1[0]=28,s2[0]=28;
for(int i=1;i<=m;i++)
s1[i]=(s1[i-1]*chr%mod1+t[i])%mod1,
s2[i]=(s2[i-1]*chr%mod2+t[i])%mod2;
Manacher();
printf("Case #%d: %d\n",kase,S.size());
}
}
例题5 codeforces 1080e Sonya and Matrix Beauty
题意
给出一个N*M的字母矩阵,定义一个子矩阵是hjb矩阵,当你把这个矩阵行上的字符任意排列(列不能动)后,可以使得到的矩阵横看竖看都是回文的
现在问你有多少个hjb矩阵
N,M<=250
题解
我们先枚举这个子矩阵的l和r
神来之笔啊喂
首先对于行的问题,只要字符出现次数是奇数不大于1个,这个行肯定是可以使其变成回文串的
然后我们考虑纵列,由于也是回文,所以肯定是对称的,那么如果要对称,这两行肯定各个字母数量都是相等的,而且如果数量相当,就一定能对称
这样看来,我们似乎可以直接干脆用各个字母数量来重新编号(注意要把那些不合法的改成其他的东西),就在纵列上做马拉车,求出纵列上的回文串数量,就是这个范围内的子矩阵数量
然后我就T了
主要是贪省事,直接扔进map重新编号…
然而我们最好是搞个cmp来比较
这个部分具体见代码
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<map>
#include<vector>
using namespace std;
typedef long long ll;
const int N=505,M=26;
int n,m;
char s[N][N];
int sum[N][N][M];
int p[N*2];
bool ban[N];
int L,R;
bool cmp(int x,int y){
if(x==0||y==0)
return false;
if(x&1&&y&1)
return true;
if(x&1||y&1)
return false;
x/=2,y/=2;
if(ban[x]||ban[y])
return false;
for(int i=0;i<26;i++)
if(sum[x][R][i]-sum[x][L-1][i]!=sum[y][R][i]-sum[y][L-1][i])
return false;
return true;
}
void Manacher(int len){
int mx=0,id=0;
for(int i=1;i<=len;i++){
if(i<mx)
p[i]=min(mx-i+1,p[id*2-i]);
else
p[i]=1;
while(cmp(i-p[i],i+p[i]))
p[i]++;
if(p[i]+i-1>mx){
mx=p[i]+i-1;
id=i;
}
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%s",s[i]+1);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++){
memcpy(sum[i][j],sum[i][j-1],sizeof sum[i][j]);
sum[i][j][s[i][j]-'a']++;
}
int ans=0;
for(int l=1;l<=m;l++)
for(int r=l;r<=m;r++){
L=l,R=r;
memset(ban,0,sizeof ban);
for(int k=1;k<=n;k++){
int f=0;
for(int z=0;z<26;z++){
if((sum[k][r][z]-sum[k][l-1][z])&1)
f++;
}
if(f>1)
ban[k]=1;
}
int len=2*n+1;
Manacher(len);
for(int i=2;i<len;i++)
if(i&1||!ban[i/2]){
if(i&1)
ans+=(p[i]-1)/2;
else
ans+=p[i]/2;
}
}
printf("%d\n",ans);
}