POJ:3974 Palindrome (Manacher算法)

Manacher算法:在O(n)的时间内求得最长回文子串。

 

在这里简单说一下这个算法。

首先在原字符串s之间加入一个特殊字符(原串中没有的)#作为标记构造一个新串ss。这样做可以其实可以把最长回文子串是奇数偶数两种情况合并为只有奇数一种情况。

然后,开一个数组p[]来保存以每个字符为中心的最长回文子串的长度。

        下面进入关键的部分。

关于求以字符s[i]为中心的最长回文串p[i]方法很简单,只要以s[i]为中心,判断s[i-(r-1)]==s[i+(r-1)]枚举回文串的半径长度r(从1开始),也就是说得到使s[i-(r-1)]==s[i+(r-1)]成立的最大的r即可。用这种方法可以想到遍历整个字符串s来得到每个p[i],这样时间复杂度是O(n^2)。

但其实这样做了很多重复的匹配。

比如说,字符串s(假设下标从1开始):AAABAAAC 。我们对于s[2](A),求得了p[2]=2。那么当我们求p[6]的时候,r还需要从1开始枚举吗?很明显s[2]和s[6]是关于4位置对称的,那么要求p[6],半径r至少要从p[2]=2开始枚举啊.显然对于上述字符串这种情况是符合的。

那么再举一个例子,字符串s:AAABAAC。p[2]依旧等于2,它的对称位置p[6]却显然等于1,从2开始枚举半径r显然是不对的。

对比上述两个例子,我们可以发现一个不同点,对于第二个例子,对于6位置,6+2-1=7已经超出了以B为中心的最长回文边界位置6了,s[7]还没有匹配过,我们怎么知道它是不是s[6]的回文呢。

这里就可以发现我们应该从min{最长回文边界 到 当前中心字符位置 的距离,以 对称字符 为中心的 最长回文子串长度}枚举r的值,这样就可以减少许多不必要的匹配。

贴一张图片以方便理解这个地方。

POJ:3974 Palindrome (Manacher算法)_第1张图片

再看这个减少不必要匹配的过程,都是在一个大的回文串里面进行的,整个回文串长度已确定,它对应中心的字符以左的p[]也都已经计算出来了,然后计算之右的。

为了尽量每次都能减少不必要的匹配,我们应该保证这个大的回文串尽量靠右。这是贪心的思想。

 

上面是整个算法的核心。

为了方便解释我忽略了其中关于标记#的部分,另外转一篇文章会有关于这个的解释。

 

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
#define MAXN 1000010
using namespace std;
char s[MAXN];
char ss[MAXN<<1];
int dp[MAXN<<1];
int Manacher(int len)
{
    int right=0,id=0,ans=0;
    int r;
    for(int i=0; i<len; ++i)
    {
        if(i<=right)
            r=max(1,min(right-i+1,dp[2*id-i]));
        while(i-(r-1)>=0&&i+(r-1)<len&&ss[i-(r-1)]==ss[i+(r-1)]) r++;
        r--;
        dp[i]=r;
        if(i+(r-1)>right)
        {
            right=i+r-1;
            id=i;
        }
        ans=max(r,ans);
    }
    return ans-1;
}
int main()
{
    int kase=0;
    while(scanf("%s",&s)&&(strcmp(s, "END")))
    {
        memset(ss,0,sizeof(ss));
        memset(dp,0,sizeof(dp));
        int L=strlen(s);
        for(int i=0; i<L; ++i )
        {
            ss[2*i]='#';
            ss[2*i+1]=s[i];
        }
        ss[2*L]='#';
        printf("Case %d: %d\n", ++kase,Manacher(2*L+1));
    }
    return 0;
}


 

你可能感兴趣的:(最长回文子串,Manacher)