Manacher(马拉车)算法

Manacher算法是一个求字符串的最长回文子串一种非常高效的方法,其时间复杂度为O(n)。下面分析以下其实行原理及代码:

1.首先对字符串进行预处理

因为回文分为奇回文和偶回文,分类处理比较麻烦,所以我们先要做一个预处理,在字符之间插入一个特殊字符(注意这个新插入的字符不能再原字符串中出现),这样无论原字符串是奇是偶,我们将它统一的转化为奇串。初此之外,我们还需在最开头也加一个特殊字符为了防止越界,例如:

原字符串 s =” abbaTNTabcba ” ,则经过转化后这个字符串会转变为:

sNew= “$#a#b#b#a#T#N#T#a#b#c#b#a#”

2.引入一个和处理过后长=长度相同的数组记录

引入一个数组假设计为p[snew.length()],其中 p[i] 表示以snew[i]为中心,半径为p[i]的最长回文子串,p[i]=1 则表示该回文子串就是senw[i]本身,下面是snew的最长回文子串半径:

由上图可知,snew[20]='c' 为中心的最长回文子串半径为6,由于第一个和最后一个字符都是#号,且也需要搜索回文,为了防止越界,由于字符串在结尾有’\0’,所以在字符串开头需要加上非#号字符(为了区分这里用的$)。通过p数组可以找到最大回文子串半径的最大值及其中心位置,就能确定最长回文子串了。所以现在问题转化为求p数组。

3.p数组(最长回文子串半径数组)的求法

Manacher算法利用开头提到的回文的左边是右边的镜像,让回文串起始的对比位置尽可能的大:

这里引入了两个新的变量id和mx,id为最大回文串中心的位置,mx为最大回文串的右边界,i为当前遍历带字符串的为位置。

这里分两种讨论:

一、mx > i

假设当前遍历到字符串的位置i,由于在遍历到id位置的时候已知最大回文子串,位置i还在上一个最大回文子串的范围内,所以可以利用其镜像认为,位置i以id为中心镜像到另一边的位置j是对等的。 在mx>i的条件下,又分为以下两种情况:

<1.mx - i > p[j] (图1)

此时,以j为中心的回文子串包含在以id为中心的回文子串内,由于i和j位置对等,所以以i为中心的回文子串包含在以id为中心的回文子串内,所以p[i] = p[j] = p[2 * id - i]。

<2. mx - i <= p[j] (图2)

此时,以j为中心的回文子串超过了以id为中心的回文子串边界,但是由于i和j位置对等,绿框部分还是相同的。所以其向右延伸的范围最大就是mx-i,剩下超过的部分谁也不能保证是否一致,只能通过循环对比判断,所以p[i] = mx - i。  

二、mx < i

此时镜像位置对预判位置起不到作用,只能从长度为1进行对比,所以此时p[i]=1; 

下面为实现代码:

#include<string>
#include <vector>
#include<iostream>  
#include <algorithm>

using namespace std;

string Manacher(string s)
{
	string snew="$#";			//预处理,只要在原字符串中不可能出现即可
	int len=s.length();
	for (int i=0;i<len;i++)
	{
		snew+=s[i];
		snew+="#";
	}
	int len2=snew.length();
	int lenans=-1;		    // 最长回文子串的长度
	int pos=-1;				// 最长回文子串中心点的位置
	vector<int> p(len2, 0);
	int id=0;					//当前中心点的位置
	int mx=0;					//最大回文子串的右边界
	for (int i=1;i<len2;i++)
	{
		if (i<mx)
			p[i]=min(p[2*id-i],mx-i);
		else
			p[i]=1;
		while(snew[i-p[i]]==snew[i+p[i]])  //最左边sNew[0]='$',最右边sNew[sNew.size()] = '\0',无需判断边界
			p[i]++;
		if(p[i]+i>mx)  
//我们每走一步i,都要和mx比较,我们希望mx尽可能的远,这样才能更有机会执行if(i<mx)这句代码,从而提高效率 
		{
			id=i;
			mx=i+p[i];
		}
		if (p[i]-1>lenans)
		{
			lenans=p[i]-1;
			pos=i;
		}
	}
	string::iterator iStart=s.begin()+(pos-lenans-1)/2;  
    				//将最长回文子串起始位置转换回原串
	return string(iStart,iStart+lenans);						   
    				//也可以return lenans,最长回文子串的长度
}

int main()
{
	string s;
	cin >> s;
	cout << s << " 的最长回文子串为: " << Manacher(s) << endl;
	return 0;
}
posted @ 2020-02-02 13:29  StungYep  阅读(128)  评论(0编辑  收藏  举报