最长回文子串-Manacher算法(详解)

北城以北 提交于 2019-12-04 20:14:39

定义:

回文串:一个字符串, 逆置之后,与原串相同;
回文子串: 一个字符串的子串(连续),是回文串.则该子串为整个字符串的一个回文子串.
最长回文子串:一个字符串中最长的回文子串.

求最长回文子串最容易

方法1(dp):

先将串逆置,再与原串求最长公共子序列(LCS)(o(n^2)), //时间O(n^2) 空间O(f(n^2));

方法2(纯暴力):

两重循环枚举起点终点(所有子串)(o(n^2)),如果是回文串,返回长度,不是返回0(o(n)),寻找最大长度.//时间 O(n^3);

稍微提升一下,

方法3(中心扩展):

枚举对称中心, 向两边扩展,遇边界,或不同,停止扩展,继续下一个对称中心,之后找最大//O(n^2);

方法4(dp):

暴力改进: 辅助数组记录从i到j是否回文
P(i,j)= P(i+1,j-1)(如果S[i] = S[j])。类动规方程。
P(i,i)= 1,P(i,i+1)= if(S[i]= S[i+1]) 个人感觉没LCS好写,好想.

下面进入正题,传说ManacherO(n),你信吗? 反正我不信.

我看了下, 别人的博客上来就说要分奇数偶数,然后加’#’,然后谁小于等于谁,然后贴代码.
所以决定认真写下本篇博客.解除疑惑.
想写代码,首先要懂道理,看别人代码,花了好长时间看懂了,昂,是这么一回事,等让自己写原题时,能凭印象写出来,但是为什么题目稍微变一变就不会了?理解的不透彻,不深入,一定要完全弄懂道理后,自己将算法转化为代码,自己写模板,再看别人怎么写的,这样写有什么优点,再感悟,再理解,更新自己的模板,这也是鄙人的学习算法的方法.

不扯了.

首先manacher来自于中心扩展思想,所以首先要把中心扩展理解透彻.这里给出中心扩展的代码,分奇数偶数.下面写下伪代码,思路已经很清晰了,枚举中心位置,找到最长,更新最长.

//char a[N];
//cin>>a; 
int find()
{
    int maxlen = 0, len = strlen(a);
//--------------------------奇数-------------------------------------------
    for(int i = 0; i < len; i++)
    {
        int l = i-1, r = i+1;
        while(l >=0 && r <= len-1 && a[l] == a[r])
        {
            if(r-l + 1 > maxlen) maxlen = r-l+1;
            l--; r++;
        }
    }
//-------------------------------------------------------------------------
//--------------------------偶数-------------------------------------------
    for(int i = 0; i < len; i++)
    {
        int l = i, r = i+1;
        while(l >= 0 && r <=len-1 && a[r]==a[l])
        {
            if(r-l+1 > maxlen) maxlen = r-l+1;
            l--; r++;
        }
    }
//-------------------------------------------------------------------------
    return maxlen;
}

可以在上述代码中加上若干printf语句调试,,有没有感觉到,上述代码做了很多没必要的比较,? 好,优化!
首先是时候对奇数偶数说一声拜拜了,中心扩展,首先两个字中心!!,,偶数没有中心.所以仔细考虑一下,利用了一个很显然的数学道理
:奇数加偶数一定是奇数 ,偶数加奇数一定是奇数. 所以我们在字符串开头结尾添加#,每个字符中间加#,一共加了strlen()+1个 #
,最后得新字符串长度:2(strlen()) + 1 必为奇数. 例:我们给他加一个中心”aa” ——>”^a^a^” (#a#a#)
都可以,从而将偶数转奇数 对于奇数”a” —> “#a#” 还是奇数

观察:

原串:qwecb abcccOcccba ckpoiu
上述代码中每次求出的l-r+1:(枚举每个字符为中心进行扩展的回文串长度) 为:
q w e c b a b c c c O c c c b a c k p o i u
1,1, 1,1,1,5,1,1,3,1,11,1,3,1,1,1,1,1,1,1,1,1 max: 11

这里写图片描述

看一下其中有没有什么空子,可不可以偷懒.
发现,对称位置有懒可以偷

这里写图片描述

k1, k2 在s(回文)之中,所以,以k1中间的c与以k2中间的c 为中心的回文子串长度必然相等..

manacher的懒就偷再这了.

(1)Len数组简介与性质

这里写图片描述

Manacher算法用一个辅助数组Len[i]表示以字符T[i]为中心的最长回文字串的最右字符到T[i]的长度,比如以T[i]为中心的最长回文字串是T[l,r],那么Len[i]=r-i+1。
对于上面的例子,可以得出Len[i]数组为:

这里写图片描述

Len
数组有一个性质,那就是Len[i]-1就是该回文子串在原字符串S中的长度,至于证明,首先在转换得到的字符串T中,所有的回文字串的长度都为奇数,那
么对于以T[i]为中心的最长回文字串,其长度就为2*Len[i]-1,经过观察可知,T中所有的回文子串,其中分隔符的数量一定比其他字符的数量多
1,也就是有Len[i]个分隔符,剩下Len[i]-1个字符来自原字符串,所以该回文串在原字符串中的长度就为Len[i]-1。

有了这个性质,那么原问题就转化为求所有的Len[i]。下面介绍如何在线性时间复杂度内求出所有的Len。

(2)Len数组的计算

首先从左往右依次计算Len[i],当计算Len[i]时,Lenj已经计算完毕。设P为之前计算中最长回文子串的右端点的最大值,并且设取得这个最大值的位置为po,分两种情况:

第一种情况:i<=P

这里写图片描述

那么找到i相对于po的对称位置,设为j,那么如果Len[j] < P-i,如上图: 那
么说明以j为中心的回文串一定在以po为中心的回文串的内部,且j和i关于位置po对称,由回文串的定义可知,一个回文串反过来还是一个回文串,所以以i
为中心的回文串的长度至少和以j为中心的回文串一样,即Len[i] >= Len[j]。 由对称性可知Len[i]=Len[j]。

如果Len[j]>=P-i,由对称性,说明以i为中心的回文串可能会延伸到P之外,而大于P的部分我们还没有进行匹配,所以要从P+1位置开始一个一个进行匹配,直到发生失配,从而更新P和对应的po以及Len[i]。

第二种情况: i>P

如果i比P还要大,说明对于中点为i的回文串还一点都没有匹配,这个时候,就只能老老实实地一个一个匹配了,匹配完成后要更新P的位置和对应的po以及Len[i]。

这里写图片描述

题目:
hihocoder 1032 : 最长回文子串

代码:

/*************************************************************************
        > File Name: 1032.cpp
      > Author: dulun
      > Mail: dulun@xiyoulinux.org
      > Created Time: 2016年04月10日 星期日 00时52分10秒
 ************************************************************************/

#include<iostream>
#include<stdio.h>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#define LL long long
using namespace std;

const int N = 1e6+9;
char a[N];
char t[N*2];
int cnt[N*2];

void change()
{
    int len = strlen(a);
    t[0] = '@';
    for(int i = 0; i < len; i++)
    {
        t[i*2+1] = '#';
        t[i*2+2] = a[i];
    }
    t[len*2+1] = '#';
    t[len*2+2] = '\0';
}

int manacher()
{
    memset(cnt, 0, sizeof(cnt));
    cnt[0] = cnt[1] = 1;
    int id = 1;
    int ans = 1;
    int len = strlen(t);
    for(int i = 2; i <= len; i++)
    {
        int num = min(cnt[id*2-i], cnt[id]+id-i);
        while(t[i-num] == t[i+num])
            num++;
        cnt[i] = num;
        if(id+cnt[id] < i+num)
            id = i;
        if(ans<num)
            ans = num;
    }
    return ans-1;
}

int main()
{
    int T;
    cin>>T;
    while(T--)
    {
        memset(t, 0, sizeof(t));
        memset(a, 0, sizeof(a));

        int ans;
        scanf("%s", a);
        change();
        printf("%d\n", manacher());
    }
    return 0;
}

改编自:http://blog.csdn.net/dyx404514/article/details/42061017

    const int maxn=1000010;  
    char str[maxn];//原字符串  
    char tmp[maxn<<1];//转换后的字符串  
    int Len[maxn<<1];  
    //转换原始串  
    int INIT(char *st)  
    {  
        int i,len=strlen(st);  
        tmp[0]='@';//字符串开头增加一个特殊字符,防止越界  
        for(i=1;i<=2*len;i+=2)  
        {  
            tmp[i]='#';  
            tmp[i+1]=st[i/2];  
        }  
        tmp[2*len+1]='#';  
        tmp[2*len+2]='$';//字符串结尾加一个字符,防止越界  
        tmp[2*len+3]=0;  
        return 2*len+1;//返回转换字符串的长度  
    }  
    //Manacher算法计算过程  
    int MANACHER(char *st,int len)  
    {  
         int mx=0,ans=0,po=0;//mx即为当前计算回文串最右边字符的最大值  
         for(int i=1;i<=len;i++)  
         {  
             if(mx>i)  
             Len[i]=min(mx-i,Len[2*po-i]);//在Len[j]和mx-i中取个小  
             else  
             Len[i]=1;//如果i>=mx,要从头开始匹配  
             while(st[i-Len[i]]==st[i+Len[i]])  
             Len[i]++;  
             if(Len[i]+i>mx)//若新计算的回文串右端点位置大于mx,要更新po和mx的值  
             {  
                 mx=Len[i]+i;  
                 po=i;  
             }  
             ans=max(ans,Len[i]);  
         }  
         return ans-1;//返回Len[i]中的最大值-1即为原串的最长回文子串额长度   
      }  
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!