说这个算法之前先来说一下暴力中心扩展算法求解最长回文子串问题,就是我们枚举每一个中心点,然后依次暴力向外扩展直到遇到两端字符不相同时停止扩展,就得到了以当前字符为中心时的最长回文子串,最长回文子串为奇数和偶数的情况分别考虑一下即可。我们可以把一个字符串添加一些字符,使其偶回文子串可以变成奇回文子串,就在字符串每两个相邻字符之间添加一个‘#’,然后在字符串开头和结尾再分别添加一个字符即可。比如字符串cbbc,添加后得到#c#b#b#c#,这样原来偶回文串的中心就变为以#为中心的奇回文串了,这样我们就可以只研究最大奇回文子串的求法了。
Manacher算法就是对暴力中心扩展算法进行了优化,充分利用了回文串对称的性质来进行优化,最终复杂度是O(n)
记p[i]表示第i个字符到可以扩展到的最右边的字符的距离,包含第i个字符。
id是回文子串中右边界最大的回文子串的中心,mx是以id为中心的最大回文子串的右边界的下一个字符。
由于我们是从前往后遍历的,假设当前遍历到了第i个字符,那么对于所有的p[1~i-1]都是已知的。
如果i<mx,那么对应着下图的情况:
那么我们可以找出来i关于id的对称点j,也就是2*id-i,由对称性可以知道如果j的最大回文子串不超过以id为中心的最大回文子串,那么i的回文子串就与j相同,记i到mx的距离为mx-i,如果j的最大回文子串不在以id为中心的最大回文子串中,那么i的最大回文子串的右边界就一定是mx-1,因为因为mx是以id为中心扩展不到的第一个位置,说明mx所在位置的字符与mx关于id对称得到的位置的字符是不同的,而由于j的最大回文子串包含mx关于id对称得到的位置的字符,所以i的最大回文子串就不会包含mx位置所对应的字符,那么总的来说,p[i]=min(p[j],mx-i)
如果i>=mx,也就是说i不在之前所得到的所有回文子串中,所以只能设其p数组为1
然后我们只能暴力扩展求得其p数组,由于mx是递增的,最大扩展n次,所以总的复杂度就是O(n)
最后注意一点,为了使得我们不用处理边界问题,我们可以令两个边界是我们没用过的字符。
下面给出一道洛谷模板题链接:【模板】manacher 算法 - 洛谷
细节见代码:
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<map>
#include<queue>
#include<vector>
#include<cmath>
using namespace std;
const int N=3e7+10;
char s[N];
int p[N],mx,id;
int main()
{
scanf("%s",s+1);
int n=strlen(s+1);
for(int i=n;i>=0;i--)
{
s[2*i]=s[i];
s[2*i+1]='#';
}
s[0]='$';
for(int i=1;i<=2*n+1;i++)
{
if(i<mx) p[i]=min(p[2*id-i],mx-i);
else p[i]=1;
while(s[i-p[i]]==s[i+p[i]]) p[i]++;
if(i+p[i]>mx)
{
id=i;
mx=i+p[i];
}
}
int ans=0;
for(int i=1;i<=2*n+1;i++) ans=max(ans,p[i]-1);
printf("%d",ans);
return 0;
}