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)\) 的。
例题
首先跑一遍 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;
}
如果把一个串和其反串拼接,形成的必为回文串。如果要更短,这两个串需要重叠一部分,这一部分为最长后缀回文串。用 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;
}
由于只用算奇数长度的回文串,可以不添加特殊字符。
统计每种长度的回文串数量时,如果存在半径为 \(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;
}
[[字符串]]