Manacher算法 学习笔记

概述

Manacher 算法可以在 \(O(n)\) 的复杂度中求出一个字符串的最长回文子串。

过程

回文串有奇数长度和偶数长度两种,这就比较麻烦。如果把每两个字符之间加一个特殊字符,就只有奇数长度的回文子串了。这里为了方便处理边界还在首尾各加了一个特殊字符。

cin>>temp+1,n=strlen(temp+1),s[0]=s[1]='#';
for(int i=1;i<=n;i++)s[i*2]=temp[i],s[i*2+1]='#';

然后考虑怎么算奇数长度的回文子串。设以位置 \(i\) 为中心的回文子串的最大半径为 \(d_i\)。暴力的做法是让每个 \(d_i\)\(1\) 开始暴力拓展。

void brute_force(int n,char s[],int d[]){
  for(int i=0;i<=n;i++){
    d[i]=1;
    while(s[i+d[i]]==s[i-d[i]])d[i]++;
  }
}

如图,考虑递推。设当前最靠右的右端点为 \(r\),这个回文子串的中心为 \(mid\)。如果 \(i<r\),那么根据对称性,可以先把 \(d_i\) 赋值为 \(d_{2mid-1}\) 再暴力拓展。

然而如果 \(2mid-1\) 的回文子串超出了 \(mid\) 的范围,就不能要超出的部分。因此对 \(r-i\)\(\min\)

void manacher(int n,char s[],int d[]){
  for(int i=0,r=0,mid;i<=n;i++){
    d[i]=i<r?min(d[mid*2-i],r-i):1;
    while(s[i+d[i]]==s[i-d[i]])d[i]++;
    if(i+d[i]>r)r=i+d[i],mid=i;
  }
}

复杂度证明:类似拓展 KMP,while 每执行一次 \(r\) 会增加一,但是 \(r\leq n\)。因此是均摊 \(O(n)\) 的。

例题

P4555

首先跑一遍 Manacher,用两个回文串拼成一个双回文串。因为两个串不能重叠,所以只考虑断点为特殊字符的情况。

对每个位置处理两个信息 \(l,r\),分别表示左端点、右端点在这个位置上的最大长度。首先有 \(l_{i-d_i+1}=\max\{d_i-1\},r_{i+d_i-1}=\max\{d_i-1\}\)

但是这样只算了每个位置上的最长回文子串。考虑递推,有 \(l_i=\max(l_i,l_{i-2}-2),r_i=\max(r_i,r_{r+2}-2)\)。然后就可以 \(O(n)\) 枚举断点了。

#include<bits/stdc++.h>
using namespace std;
int n,d[200005],ans,l[200005],r[200005];
char temp[100005],s[200005];
void manacher(int n,char s[],int d[]){
  for(int i=1,r=0,mid;i<=n;i++){
    d[i]=i<r?min(d[mid*2-i],r-i):1;
    while(s[i+d[i]]==s[i-d[i]])d[i]++;
    if(i+d[i]>r)r=i+d[i],mid=i;
  }
}
int main(){
  cin>>temp+1,n=strlen(temp+1),s[0]=s[1]='#';
  for(int i=1;i<=n;i++)s[i*2]=temp[i],s[i*2+1]='#';
  manacher(2*n+1,s,d);
  for(int i=1;i<=2*n+1;i++)l[i-d[i]+1]=max(l[i-d[i]+1],d[i]-1),r[i+d[i]-1]=max(r[i+d[i]-1],d[i]-1);
  for(int i=3;i<=2*n+1;i+=2)l[i]=max(l[i],l[i-2]-2);
  for(int i=2*n-1;i>=1;i-=2)r[i]=max(r[i],r[i+2]-2);
  for(int i=1;i<=2*n+1;i+=2)if(l[i]&&r[i])ans=max(ans,l[i]+r[i]);
  return cout<<ans<<'\n',0;
}

UVA11475

如果把一个串和其反串拼接,形成的必为回文串。如果要更短,这两个串需要重叠一部分,这一部分为最长后缀回文串。用 Manacher 可以求。

#include<bits/stdc++.h>
using namespace std;
int n,d[22000005];
char temp[11000005],s[22000005];
void manacher(int n,char s[],int d[]){
  for(int i=0,r=0,mid;i<=n;i++){
    d[i]=i<r?min(d[mid*2-i],r-i):1;
    while(s[i+d[i]]==s[i-d[i]])d[i]++;
    if(i+d[i]>r)r=i+d[i],mid=i;
  }
}
int main(){
  while(cin>>temp+1){
    n=strlen(temp+1),s[0]=s[1]='#';
    for(int i=1;i<=n;i++)s[i*2]=temp[i],s[i*2+1]='#';
    manacher(2*n+1,s,d);
    for(int i=1;i<=2*n+1;i++){
      if(i+d[i]-1==2*n+1){
        cout<<temp+1;
        for(int j=n-d[i]+1;j>=1;j--)putchar(temp[j]); 
        putchar('\n');
        break;
      }
    }
  }
  return 0;
}

P1659

由于只用算奇数长度的回文串,可以不添加特殊字符。

统计每种长度的回文串数量时,如果存在半径为 \(d_i\) 的回文串,也存在半径为 \(1,2,\cdots,d_i-1\) 的回文串。因此将 \(cnt\) 做一遍前缀和才是真正的次数。然后扫一遍统计即可。

#include<bits/stdc++.h>
using namespace std;
template<typename T>T qpow(T a,T b,T n,T ans=1){
  for(a%=n;b;b>>=1)b&1&&(ans=1ll*ans*a%n),a=1ll*a*a%n;
  return ans;
}
int n,d[1000005],cnt[1000005];
long long k,ans=1,sum;
char s[1000005];
void manacher(int n,char s[],int d[]){
  for(int i=0,r=0,mid;i<=n;i++){
    d[i]=i<r?min(d[mid*2-i],r-i):1;
    while(i+d[i]<=n&&i-d[i]>=1&&s[i+d[i]]==s[i-d[i]])d[i]++;
    if(i+d[i]>r)r=i+d[i],mid=i;
  }
}
int main(){
  cin>>n>>k>>s+1,manacher(n,s,d);
  for(int i=1;i<=n;i++)cnt[d[i]]++;
  for(int i=n;i>=1&&k;i--)sum+=cnt[i],ans=(ans*qpow((long long)i*2-1,min(k,sum),19930726ll))%19930726,k-=min(k,sum);
  return cout<<(k?-1:ans)<<'\n',0;
}

[[字符串]]

posted @ 2024-03-01 09:38  lgh_2009  阅读(1)  评论(0编辑  收藏  举报