manacher
yanmingqian · · 算法·理论
引入
一种求字符串中最长回文串的算法,好像是今年刚被扔到提高级大纲的。
先来引入一个很好的记录回文串方式:我们定义
显然有朴素的时间复杂度
解法
考虑更好的做法。manacher 就是一种实现异常简单的解决这种问题的算法,而且拥有高贵的
首先我们要对原始字符串做一点小处理。如果我们直接求一个字符串的回文串,那么我们需要分为回文串长度为奇数和长度为偶数的情况进行讨论,这样比较麻烦。有一种更好的解决方式:我们在原串的开头结尾以及两两字符之间插入一个原串中不存在的特殊字符,如 #。这样我们再对这个新字符串进行求解,就能够让所有回文串长度都变成奇数,会方便很多。对于长度的影响是每个回文串的半径变为了原来的两倍加一。
接下来是算法的核心。我们假设现在枚举到了
我们来梳理一下算法思路:
- 设
k 为当前的d_i 。若i\le r ,先将k 设为\min(d_{2\times mid -1},r-i) ,否则将k 设为1 ; - 调用朴素算法,暴力扩展
k ; - 更新
mid 和r 。
代码如下:
int n=s.size(),mid=0,r=-1;
for(int i=0;i<n;i++){
int k=(i<=r)?min(d[mid*2-i],r-i+1):1;
while(i>=k&&i+k<n&&s[i-k]==s[i+k]) k++;
d[i]=k--;
if(i+k>r) r=i+k,mid=i;
}
时间复杂度
这个算法乍一看好像只是朴素算法的优化,可能会退化为
题目
P3805 【模板】manacher
给出一个只由小写英文字符
\texttt a,\texttt b,\texttt c,\ldots\texttt y,\texttt z 组成的字符串S ,求S 中最长回文串的长度。
板子,直接套就行。注意我们求得的
需要注意的是,一开始在两两字符间插入特殊字符时,不要使用 s=s+c+'#',而应使用 s+=c,s+='#'。前者很慢。
#include<iostream>
using namespace std;
const int N=2.2e7+10;
int d[N];
int main(){
string s="#";char c=getchar();
while(c>='a'&&c<='z') s+=c,s+='#',c=getchar();
int n=s.size(),mid=0,r=-1,ans=0;
for(int i=0;i<n;i++){
int k=(i<=r)?min(d[mid*2-i],r-i+1):1;
while(i>=k&&i+k<n&&s[i-k]==s[i+k]) k++;
ans=max(ans,d[i]=k--);
if(i+k>r) r=i+k,mid=i;
}
cout<<ans-1;
return 0;
}
P1659 [国家集训队] 拉拉队排练
给定长度为
n 的字符串S 和k ,求S 中所有回文串的长度中前k 大的乘积模19930726 的值,回文串数不足k 个输出-1 。
先 manacher,然后我们用一个桶来记录每个
#include<iostream>
using namespace std;
#define int long long
const int N=1e6+10,mod=19930726;
int d[N],t[N];
int qpow(int a,int b){
int ans=1;
while(b){
if(b&1) ans=ans*a%mod;
a=a*a%mod;b>>=1;
}
return ans;
}
signed main(){
int n,k;string s;
cin>>n>>k>>s;
int mid=0,r=-1;
for(int i=0;i<n;i++){
int k=(i<=r)?min(d[mid*2-i],r-i+1):1;
while(i>=k&&i+k<n&&s[i-k]==s[i+k]) k++;
d[i]=k--;
if(i+k>r) r=i+k,mid=i;
t[d[i]*2-1]++;
}
if(!(n&1)) n--;
int sum=0,ans=1;
for(int i=n;i>=1;i-=2){
sum+=t[i];
if(sum>k){ans=ans*qpow(i,k)%mod;break;}
else ans=ans*qpow(i,sum)%mod,k-=sum;
}
cout<<((sum>=k)?ans:-1);
return 0;
}
P4555 [国家集训队] 最长双回文串
输入长度为
n 的串S ,求S 的最长双回文子串T ,即可将T 分为两部分X, Y (|X|,|Y|≥1 )且X 和Y 都是回文串。
我们考虑记录以
#include<iostream>
using namespace std;
const int N=2e5+10;
int d[N];
int ld[N],rd[N];
int main(){
string s="#";char c=getchar();
while(c>='a'&&c<='z') s+=c,s+='#',c=getchar();
int n=s.size(),mid=0,r=-1,ans=0;
for(int i=0;i<n;i++){
int k=(i<=r)?min(d[mid*2-i],r-i+1):1;
while(i>=k&&i+k<n&&s[i-k]==s[i+k]) k++;
d[i]=k--;
if(i+k>r) r=i+k,mid=i;
ld[i-k]=max(ld[i-k],k);rd[i+k]=max(rd[i+k],k);
for(int i=2;i<n;i+=2) ld[i]=max(ld[i],ld[i-2]-2);
for(int i=n-1;i>=0;i-=2) rd[i]=max(rd[i],rd[i+2]-2);
for(int i=0;i<n;i++) if(ld[i]&&rd[i]) ans=max(ans,ld[i]+rd[i]); //注意必须ld[i]和rd[i]均不为0时才可统计
cout<<ans;
return 0;
}
P4287 [SHOI2011] 双倍回文
对于给定的字符串,计算它的最长双倍回文子串的长度。双倍回文串的定义如下:若
x 为双倍回文串,则其长度必须为4 的倍数,其本身为回文串,且从中间分成的前后两部分都必须同样是回文串。
我们考虑在 manacher 中每次更新
#include<iostream>
using namespace std;
const int N=1e6+10;
int d[N];
int main(){
int n;
cin>>n;
string s="#";char c=getchar();
while(c<'a'||c>'z') c=getchar();while(c>='a'&&c<='z') s+=c,s+='#',c=getchar();
int mid=0,r=-1,ans=0;n=n<<1|1;
for(int i=0;i<n;i++){
int k=((i<=r)?min(d[mid*2-i],r-i+1):1);
while(i>=k&&i+k<n&&s[i-k]==s[i+k]) k++;
d[i]=k--;
if(i+k>r){ //找到了一个新的回文串后,我们考虑在这个回文串里找新的双倍回文串(以i为中心)
if(!(i&1)) //回文串对应长度为偶数
for(int j=max(r,i+4);j<i+d[i];j++) //遍历新扩展的区域,j是潜在的双倍回文串右边界
if((j-i)%4==0&&d[i-(j-i)/2]>=(j-i)/2) //长度为4的倍数且左半部分是回文串
ans=max(ans,j-i);
r=i+k,mid=i;
}
}
cout<<ans;
return 0;
}