算法介绍
\(\text{Manacher}\) 算法(又名马拉车),是一种常用于处理回文字符串的算法。其代码量很小,却可以在 \(O(n)\) 的时间复杂度内处理问题
算法思想
和其他大多数算法一样,\(\text{Manacher}\) 算法利用现有的信息获得下一部分的信息。
经典例题:给定一个字符串 \(s\)。求出其最长回文子串
\(|s|\le 1.1\times 10^7\)。
该题暴力算法法是,枚举 \(s\) 的任意一个子串 \(s_{l,r}\),判断其是否是回文串,如果是,就用其更新答案。在不加任何优化的前提下算法时间复杂度 \(O(n^3)\)。
我们思考一个回文串的性质。对于一个回文串,其必然有一个回文中心,满足这个回文串关于回文中心对称。回文中心到字符串两端的距离成为回文半径。回文中心分为两类,分别是在一个字符上:例如 \(\texttt{ababa}\) 的回文中心在中间的 \(\texttt{a}\) 上;在者是两个字符的间隙之间,例如 \(\texttt{abba}\) 的回文中心在两个 \(\texttt{b}\) 中间。为了将问题普遍化,我们在一个字符串的任意一个字符旁插入一个不存在于这个字符串的字符。例如 \(\texttt{ababa}\) 转化为 \(\texttt{\#b\#a\#b\#a\#}\),\(\texttt{abba}\) 转化为 \(\texttt{\#a\#b\#b\#a\#}\) 这样,我们把两类问题都转换为一类问题。下面讨论的均是改变后的字符串
对于任何一个回文串,回文中心都只有一个。我们枚举回文中心,定义 \(P_i\) 表示以 \(i\) 为中心的最长回文半径,定义 \(l,r,mid\) 分别为当前匹配出最靠右的回文串 \(p\) 的左右边界和回文中心。
算法的运行,我们分两种情况
-
\(i>r\):因为 \(i\) 位于我们最靠右回文串边界之外。无法利用已知信息求解。暴力枚举其回文半径,找出 \(P_i\)。
-
\(i\le r\):因为 \(i\) 位于一个回文串内,回文中心为 \(mid\)。我们再分两种情况讨论
-
以 \(i\) 为回文中心的最长回文子串 \(t\) 完全包含在 \(p\)。因为回文串的性质。\(mid\) 左边必然有一个回文串关于 \(t\) 对应,记为 \(t'\)。因为回文串只有一个回文中心。所以 \(t'\) 的回文中心与 \(t\) 的回文中心关于 \(mid\) 对称。我们用 \(j\) 表示 \(t'\) 的回文中心。则 \(j=mid\times 2-i\)。也可以得到 \(P_i=P_{mid\times 2-i}\)
-
以 \(i\) 为回文中心的最长回文子串 \(t\) 不完全包含在 \(p\)。但 \(t\) 在 \(p\) 中回文的部分一定和 \(t'\) 对应。且 \(P_j\ge j-l+1\)。根据回文的对称性,虽然无法保证 \(P_i=P_j\)。但可以保证 \(P_i\ge j-l+1=r-i+1\)。找到 \(P_i\) 的最小值后无法保证回文半径最大,继续暴力枚举
最后,在计算完 \(P_i\) 的值后,记得更新 \(l,r,mid\)。
时间复杂度分析:
虽然有很多暴力的拓展,但每次拓展后,\(r\) 都会更新,且每次更新都是往后移。移动的次数都是暴力枚举的次数。所以 \(r\) 至多被枚举到右边界,时间复杂度 \(O(n)\)
答案计算
最后还有答案计算,我们令操作中,插入了辅助字符的字符串为 \(s\)。原串为 \(s'\)。结论:则原串中最长回文串的长度为 \(\max P_i-1\)。在 \(s'\) 中的最长回文子串长度为 \(2P_i-1\)(减去中间回文中心的重叠部分),其中每个字符的前面都对应一个辅助字符(即字符 \(\texttt{\#}\) 之类),最后还多出一个辅助字符。所以原串中最长回文子串的长度为 \(\dfrac{2P_i-2}{2}=P_i-1\)
例题:
例 \(1\):【模板】manacher
模板题
点击查看代码
#include <iostream>
#include <cstdio>
using namespace std;
const int N=3e7+10;
char s[N];
int p[N],mid,r=-1,n,ans=0;
void read(){
char ch=getchar();
s[n]='~',s[++n]='#';//首段加上一个‘~’是为了处理越界情况
while(ch>='a'&&ch<='z')s[++n]=ch,s[++n]='#',ch=getchar();
s[++n]='#';
return;
}
int main(){
read();
for(int i=1;i<=n;i++){
if(i>r)p[i]=1;
else p[i]=min(p[mid*2-i],r-i+1);
while(s[i-p[i]]==s[i+p[i]])p[i]++;
if(i+p[i]-1>r)r=p[i]+i-1,mid=i;
if(p[i]>ans)ans=p[i];
}
printf("%d\n",ans-1);
return 0;
}
例 \(2\):[国家集训队] 最长双回文串
对于双回文串中的每个“断点”(字符 #
的位置)。找到以其为左右端点的最长回文串 \(lef_i,righ_i\)。答案即为 \(\max\limits_{lef_i,righ_i\ne 0}lef_i+righ_i\)。由于在算法匹配中,只求出了一些点的 \(lef_i,righ_i\)。所以还需要递推来补全所有的点,然后再求解。
点击查看代码
#include <iostream>
#include <cstdio>
#define int long long
using namespace std;
const int N=3e5+10;
char s[N];
int n,mid,r=-1,p[N],L[N],R[N],ans;
void read(){
char ch=getchar();
s[++n]='~';s[++n]='#';
while(ch>='a'&&ch<='z')s[++n]=ch,s[++n]='#',ch=getchar();
return;
}
signed main(){
read();
for(int i=1;i<=n;i++){
if(i>r)p[i]=1;
else p[i]=min(p[mid*2-i],r-i+1);
while(s[i-p[i]]==s[i+p[i]])p[i]++;
if(i+p[i]-1>r)r=p[i]+i-1,mid=i;
L[i-p[i]+1]=max(L[i-p[i]+1],p[i]-1);
R[i+p[i]-1]=max(R[i+p[i]-1],p[i]-1);
}
for(int i=n-2;i>=2;i-=2)R[i]=max(R[i+2]-2,R[i]);
for(int i=4;i<=n;i+=2)L[i]=max(L[i-2]-2,L[i]);
for(int i=2;i<=n;i+=2)if(L[i]&&R[i])ans=max(ans,L[i]+R[i]);
printf("%lld\n",ans);
return 0;
}
例 \(3\):[国家集训队] 拉拉队排练
\(P_i\) 并不仅仅代表以 \(i\) 为中心的最长回文半径加一。还代表原串中以 \(i\) 为回文中心的回文串的个数,因为 \([i+P_i-1,i-P_i+1],[i+P_i-2,i-P_i+2]\dots[i+1,i-1],[i,i]\) 都在最大的回文串中,回文的性质使得他们都是回文串。
如果我们在算法的过程中,用 \(sum_k\) 表示长度为 \(k\) 的回文串的差分数组。如果得到一个 \(i\) 的 \(P_i=x\),那么只需将 \(sum_x\) 加上 \(1\)。在一起计算时先计算长的回文串,在将 \(sum_x\) 加到前面去。就可以完成计算。
因为题目要求一个“小团体”长度必须为单数。所以要对一些情况特判。\(sum_i=sum_i+sum_{i+2}\)。因为 \(k\) 很大,也会用到快速幂的技巧。同时记得特判 \(-1\) 的情况。
点击查看代码
#include <iostream>
#include <cstdio>
using namespace std;
const int N=2e6+10,mod=19930726;
typedef long long ll;
ll len,k,n,mid,r=-1,p[N],maxn=0,sum[N],ans=1,z;
char s[N];
void read(){
char ch=getchar();
s[n]='~',s[++n]='#';
while(ch<'a'||ch>'z')ch=getchar();
while(ch>='a'&&ch<='z')s[++n]=ch,s[++n]='#',ch=getchar();
return;
}
ll ksm(ll a,ll b){
ll ans=1;
while(b){
if(b&1)ans=(a*ans)%mod;
a=(a*a)%mod;
b>>=1;
}
return ans;
}
int main(){
scanf("%lld %lld",&len,&k);
read();
for(int i=1;i<=n;i++){
if(i>r)p[i]=1;
else p[i]=min(p[2*mid-i],r-i+1);
while(s[i-p[i]]==s[i+p[i]])p[i]++;
if(i+p[i]-1>r)r=i+p[i]-1,mid=i;
if(s[i]!='#')sum[p[i]-1]++,maxn=max(maxn,p[i]-1);
}
for(int i=maxn;i>=1&&k>0;i--){
sum[i]+=sum[i+2],z=min(k,sum[i]);
ans=(ans*ksm(i,z))%mod;
k-=z;
}
if(k>0)printf("-1\n");
else printf("%lld\n",ans);
return 0;
}
The End
感觉还比较有用
标签:int,Manacher,sum,mid,算法,ans,回文 From: https://www.cnblogs.com/zuoqingyuan/p/18367053