Manacher算法——求最长回文子串

    Manacher算法是用来求解一个给定字符串的最长回文子串,回文就是将字符串翻转后,与原来一样。这个算法通过添加字符,巧妙的将回文长度为偶数的情况转化成奇数,简化了问题的步骤,并且利用回文的性质,将去除了冗余的比较,从而将复查度降到O(len),能够求出以每个字符为中心的最长回文,从而求出最长的回文子串。

现在,简述一下算法的过程:

  1. 在相邻的两个字符之间(包括首尾)添加特殊字符(也就是原串不会出现的同一个字符,比如'#'),通过这个简单的步骤,所有的回文都变成奇数长度了,这个可以简单证明下,假设回文串长度是偶数 n,那么在它们之间加入特殊字符的个数为n-1 + 2,n-1个间隙,还有首尾,所以这时长度为n+n+1=2*n+1为偶数,同理,奇数长度添加后为2*n+1还是奇数。也就是我们只需要求出长度为奇数的回文,而不用考虑偶数长度了。
  2. 假设,以字符s[ i ]为中心的最长回文为 s[ i - len[ i ] , i + len[ i ] ],也就是说len[ i ]为这个回文的半径-1,由上面添加特殊字符后回文长度为2*n+1可知,2*len[ i ]+1=2*n+1,即len[ i ]就是以 i 为中心,去掉特殊字符后的回文串的长度,也就是我们要的答案。
  3. 现在是关键部分,假设,我们已知len[ j ] (1<=j<i)的值,并且知道rightmost=max{ j+len[ j ] },还有与它对应的下标 k,现在有下面几种情况:
  • rightmost<i,这种情况下,说明 i 附近的字符没有比较过,也没有已知的情况,所以len[ i ]=0,从0开始往两边比较
  • rightmost>=i,这种情况说明之前已经比较过i 附近的字符了,看下图:
Manacher算法——求最长回文子串_第1张图片
我们已知k和 i ,还有k对应的len[ k ],也就知道s[ k-len[k], k+len[k] ]是个回文,现在找到i关于k的对称点2*k-i,如果2*k-i-len[ 2*k-i ]>=k-len[k],也就是左边那个橙色的回文串没有超过k这个回文串的范围,我们立马知道,len[ i ]>=len[ 2*k-i],也就是两个橙色的串是完全相同的,因为右边这个串是左边的翻转,而橙色部分又是回文串,翻转后完全相同,所以利用回文的性质,就省去了很多比较,我们只需要从len[ i ]=len[ 2*k-i]开始比较

但是如果2*k-i-len[ 2*k-i ]<k-len[k],也就是左边的回文串超出k这个回文串的范围,如下图:
Manacher算法——求最长回文子串_第2张图片
这时i+len[2*k-i] 明显超过k+len[ k ],而超过的部分是未知的,所以要重新比较,也就是从len[ rightmost-i ] 开始比较
所以,通过两种情况可以发现,必需从len[ i ]=min( len[2*k-i] ,  rightmost-i )开始比较,直到出现左右两边不同为止,这时就是len[ i ]的值

复杂度证明:
我们可以发现,当len[2*k-i]<rightmost-i,也就是第一张图的情况,这时由于左右两边的橙色回文完全相同,并且绿色部分是翻转的,所以,len[ i ]不可能再增加,不需要匹配,复杂度为O(1),当len[2*k-i]>=rightmost-i时,len[ i ]=rightmost-i,也就是从rightmost开始比较,由于rightmost再不断递增,最多递增length(字符串S长度)次,平摊下来,每次比较的复杂度为O(1),所以总时间复杂度为length*O(1),即O(length),也就是O(n)的算法

整个算法就这么简单,模版如下:

#include <cstdio>
#include <iostream>
#include <cstring>
using namespace std;

#define N 222222
char s[N],str[N];
int ans[N];
int Manacher(char src[], char tmp[], int len[], char unique, char begin)
{
    int i,j,k,rightmost,n=strlen(src),ret=0;
    tmp[0]=begin;
    tmp[1]=unique;
    for(i=0;i<n;++i)
    {
        tmp[i*2+2]=src[i];
        tmp[i*2+3]=unique;
    }
    n=n*2+2;
    tmp[n]='\0';
    for(i=0;i<n;++i)len[i]=0;
    rightmost=k=0;
    for(i=1;i<n;++i)
    {
        if(rightmost>=i)len[i]=min(len[2*k-i],rightmost-i);
        else len[i]=0;
        for(j=len[i]+1;tmp[i+j]==tmp[i-j];++j)++len[i];
        if(i+len[i]>rightmost)
        {
            rightmost=i+len[i];
            k=i;
        }
        ret=max(ret,len[i]);
    }
    return ret;
}
int main()
{
    while(~scanf("%s",str))
        printf("%d\n",Manacher(str,s,ans,'#','$'));
    return 0;
}

对应题目:
http://acm.hdu.edu.cn/showproblem.php?pid=3068
这题就是裸的模版题


你可能感兴趣的:(Manacher算法——求最长回文子串)