最长回文子串-Manacer算法

一个字符串中连续的一段就是这个字符串的子串,而回文串指的是12421这种从前往后读和从后往前读一模一样的字符串,所以最长回文子串的意思就是这个字符串中最长的为回文串的子串。很容易想到暴力的解法,一个一个枚举回文串的起始位置。

#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
char str[1000010];

int main()
{
	int n;
	cin>>n;
	while(n--)
	{
		memset(str,0,sizeof(str));
		cin>>str;
		int ans;
		int i,j,k;
		bool flag;
		int len=strlen(str)-1;
		for(i=len;i>=0;i--)
		{
			for(j=0;j<=len;j++)
			{
				k=j+i;
				if(k<=len)
				{
					flag=1;
					int k1=k;
					int j1=j;
					while(k1>j1)
					{
						if(str[k1]!=str[j1]) flag=0;
						k1--;
						j1++;
					}
					if(flag) 
					{
						ans=i+1;
						goto end;
					}
				}
			}
		}
		end:cout<<ans<<endl;
	}
}
在这个算法中存在大量重复的计算。如果一个字符串的[3, 7]这一段已经不是回文子串了,[2, 8]这一段就不可能是回文子串了。我们可以对这个算法进行改进,枚举回文串的中点,从中点向两边扩展。值得注意的是回文串有奇数长和偶数长两种情况,对于这两种情况我们要分别进行讨论。
#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
char str[1000010];

int main()
{
	int n;
	cin>>n;
	while(n--)
	{
		memset(str,0,sizeof(str));
		cin>>str;
		int ans;
		int i,j,k,maxans=1;
		bool flag;
		int len=strlen(str)-1;
		//回文串的长度是奇数 
		for(i=0;i<=len;i++)
		{
			ans=1;
			j=i+1;
			k=i-1;
			while(j<=len&&k>=0)
			{	
				if(str[k]==str[j]) 
				{
					k--;
					j++;
					ans+=2;
				}
				else break;
			}
			if(ans>maxans) maxans=ans;
		}
		//回文串的长度是奇数
		for(i=0;i<=len;i++)
		{
			j=i+1;
			if(str[i]==str[j])
			{
				ans=2;
				j=j+1;
				k=i-1;
				while(j<=len&&k>=0)
				{	
					if(str[k]==str[j]) 
					{
						k--;
						j++;
						ans+=2;
					}
					else break;
				}
				if(ans>maxans) maxans=ans;
			}
		}
		cout<<maxans<<endl;
	}
}
下面该轮到我们要隆重介绍的Manacer算法登场了。前面我们说过枚举回文串的中点需要分情况讨论,如果在原来的字符串的前后以及每两个字符之间添加'#'字符(前提是这个'#'不在原串中出现过),显然现在所有的回文串的长度都是奇数的,这样就不用分情况讨论了,这是这个算法非常精妙的地方。我们还需要一个辅助数组p记录以每个字符为核心的最长回文字符串半径。p[i]最小为1,此时回文字符串就是字符串本身。 举个例子:
原来的串:waabwswfd
现在的串:#w #a#a #b #w#s# w#f# d#
辅助数组:1212 32 12121 41212 121
注意到,P[i]-1就是该回文子串在原串中的长度。接下来就是最关键的部分了。设id的回文串的半径是r[id],那么此时id的回文串右边要延展到right=id+r[id]-1这个字符。在计算(id, right]中的某个点x的时候,可以发现x的关于id的对称点x'=2*id-x的回文串是已经计算过的,利用x'的回文的性质,我们计算x的半径的时候,就不用从1开始枚举,而是从min(r[x'], right-x+1)开始枚举。整个过程中采用贪心法,也就是说选取right尽可能大的id使得后面的中心尽量少的扩展。自己动手画个图就很好理解了。
POJ3974就是这样的一道模板题,贴上AC代码:
#include<cstdio> 
#include<cstring>  
#include<cstdlib> 
#include<iostream>  
#include<algorithm> 
using namespace std;  
const int MAX=1000010;  
char s[MAX];  
char ss[MAX<<1];  
int p[MAX<<1];  
  
int solve(int len)  
{  
    int ans=0;  
    int right=-1;  
    int id=-1;  
    for(int i=0;i<len;i++)  
    {  
        int r=1;  
        if(right>=i) r=max(r,min(right-i+1,p[2*id-i]));  
        while((i-r+1>=0&&i+r-1<len)&&(ss[i-r+1]==ss[i+r-1])) r++;  
        r--;  
        if(i+r-1>right)  
        {  
            right=i+r-1;  
            id=i;  
        }  
        p[i]=r;  
        if(ans<r) ans=r;  
    }  
    return ans-1;  
}  
    
int main()  
{  
    int Case=1;  
    while(scanf("%s",s)!=EOF)  
    {  
        if(strcmp(s,"END")==0)  
            break;  
        int len=strlen(s);  
        int cnt=0;  
        for(int i=0;i<len;i++)  
        {  
            ss[cnt++]='#';  
            ss[cnt++]=s[i];  
        }  
        ss[cnt++]='#';  
        printf("Case %d: %d\n",Case++,solve(cnt));  
    }    
}  



你可能感兴趣的:(最长回文子串-Manacer算法)