manacher算法

manacher,又叫马拉车。

马拉车可以用来求最长回文子串。

思考如何求最长回文子串,我们首先会想到一个 \(O(n^3)\) 的解法:枚举所有子串,然后判断是否为回文串。当然这个解法非常大 蠢。
然后,我们又会有一个想法,枚举每个点,然后从这个点向外拓展,如果相同则拓展,否则记录答案。这里有个问题,就是回文串的长度可能是偶数,也就是没有中间点,那么我们可以在每两个字符之间添加 '#',这样就有了中间点了,像这样:

注意,前面的 '@' 是防止数组越界的,如果扫到头尾都是回文串,那么如果继续扫下去就会越界,而这里有一个 '@' 就可以防止越界的发生。
那么此时这个回文串真正的长度应该为 半径(包含中间点)-1。
这个算法的时间复杂度是 \(O(n^2)\)的。
我们思考上面那个算法,发现他有许多位置是重复枚举的,比如:

橙色线中间部分是一个回文串,红色线中间部分也是一个回文串,圈起来的是回文串的中心。
可以发现,这两个回文串会有重复的地方,那么有没有方法避免这些重复枚举呢?答案是有的。
首先,我们记录一个最大的 \(r\) 和对应的 \(mid\),使得 \(mid - (r - mid)\)~\(r\) 是一个回文串。(这里的 \(mid\)其实就是回文串中心的位置啦)
然后,若我们当前查询的节点 \(now\)\(r\) 之内:

那么,既然在一个回文串的右半部分(因为是从前往后扫,所以一定在 \(mid\) 右边,是有半部分),则一定有一个对应的左半部分的字符(下面简称 \(lc\) QwQ):

\(lc\) 的半径是已经求出的,若 \(lc\) 的半径是在这个大回文串之内,那么 \(now\) 半径就和 \(lc\) 的一样。但如果以 \(lc\) 为中心的字符串不全在大回文串之中呢?比如:

那么,此时我们能确定的半径就是 \(r-now\),也就是这一段:

那么,我们只要在上面两个中取一个最小值即可。

code:

#include<cstdio>
#include<string>
using namespace std;
string s;
int mid,r,ans,b[22000005];//b[i]记录以i为中心的回文串的半径
int min(int x,int y){return x<y?x:y;}
void read(string &ss)
{
	ss="@#";
	char ch=getchar();
	while(ch<'a'||ch>'z') ch=getchar();
	while(ch>='a'&&ch<='z') ss+=ch,ss+='#',ch=getchar();
	return ;
}
int main()
{
	read(s);
	for(int i=1;i<(int)s.length();i++)
	{
		if(i<=r) b[i]=min(b[mid*2-i],r-i+1);
		while(s[i-b[i]]==s[i+b[i]]) b[i]++;
		if(i+b[i]-1>r) r=i+b[i]-1,mid=i;//更新r和mid
		if(b[i]-1>ans) ans=b[i]-1;
	}
	printf("%d\n",ans);
	return 0;
}
posted @ 2020-09-06 21:01  Mine_King  阅读(109)  评论(0编辑  收藏  举报