码迷,mamicode.com
首页 > 其他好文 > 详细

最长回文子串

时间:2015-03-29 13:29:54      阅读:112      评论:0      收藏:0      [点我收藏+]

标签:

     求解字符串中最长的回文子串。

     印象中以前是做过的,用的是动态规划的区间求法,还有取中心的方法,时间复杂度O(N)的Manacher竟是闻所未闻,好好学习了一下,为之拍案叫绝。于是现在把最长回文子串的问题做个总结,以便回头翻看。

     一、枚举法

     枚举所有子串(O(N2)),判断是否为回文串(O(N)),时间复杂度为O(N3),不表。

     二、取中心点法

     枚举每一个字符str[i]看做回文子串的中心,向两边扩散,比较左端点是否等于右端点,得出每个字符做中心的最大回文长度,枚举答案即可。时间复杂度为O(N2),这个方法的好处在于好理解,看着比较舒服,本质还是枚举,枚举的一种优化嘛。但是毕竟只是O(N2)的效率,不是最优。

    附代码:

#include<stdio.h>
#include<string.h>
char str[50005];
int a[50005],n;
int func1(int k)
{
    int i,j,sum=1;
    if(k==0)return sum;
    if(k==n-1)return sum;
    i=k-1;j=k+1;
    while(i>=0 && j<n)
    {
        if(str[i]!=str[j])return sum;
        sum+=2;
        i--;j++;
    }
    return sum;
}
int func2(int k)
{
    if(k==0)return 1;
    if(k==n-1)return 1;
    if(str[k]!=str[k+1])return 0;
    int i,j,sum=2;
    i=k-1;j=k+2;
    while(i>=0 && j<n)
    {
        if(str[i]!=str[j])return sum;
        sum+=2;
        i--;j++;
    }
    return sum;

}
int main()
{
    int lp,i,j,k,sum,ans,tot,m;
    scanf("%d",&m);
    for(lp=1;lp<=m;lp++)
    {
    scanf("%s",str);
    n=strlen(str);
    memset(a,0,sizeof(a));
    for(i=0;i<n;i++)
    {
        j=func1(i);
        k=func2(i);
        a[i]=j>k?j:k;
    }
    ans=0;
    for(i=0;i<n;i++)
        if(a[i]>ans)ans=a[i];
    printf("%d\n",ans);
    }
}

 

  func1()和func2()对应着奇数回文串和偶数回文串,这样写比较顺手。总之这个方法还是比较清爽的。

     三、区间动归

     大爱dp。

     说一下动归。f[i][j]表示从i到j的子串是否是回文串,两个状态f[i][j]=1,f[i][j]=0,

     边界是:        

                    f[i][i]=1

                    if(str[i]==str[i+1])f[i][i+1]=1;即偶数回文串

     状态转移方程则是

                   f[i][j]=f[i+1][j-1]&&str[i]==str[j]

     求一下长度就好了。

     时间复杂度O(N2)。

#include<stdio.h>
#include<string.h>
int f[2010][2010],n;
char str[2010];
int main()
{
    int lp,i,j,k,m,ans=0,len;
    scanf("%d",&m);
    for(lp=0;lp<m;lp++)
    {
        scanf("%s",str);
        memset(f,0,sizeof(f));
        n=strlen(str);
        ans=0;
        for(i=0;i<n;i++)
        {
            f[i][i]=1;
            if(i<n-1&&str[i]==str[i+1])f[i][i+1]=1;
        }
        for(len=3;len<=n;len++)
            for(i=0;i<=n-len;i++)
        {
            j=i+len-1;
            if(f[i+1][j-1]&&str[i]==str[j])
            {
                ans=len;
                f[i][j]=1;
            }
        }
        printf("%d\n",ans);
    }
}

 

   重点来了。

   四、Manacher法

    时间复杂度O(N)!真是太美好了。我在理解上面有一点点卡,代码实现比较容易,非常好的方法。

    Manacher的方法是:枚举每一个点为中心的最长回文子串,在计算未知点 i 时,使用一个已知的、足够长(可以覆盖到 i 点)的回文子串(这个已知子串的中心点记为id),找出与 i 在id子串上对称的点 j(也就是以id为中心,找出一个 i 点的轴线对称点),由于这个 j 是已知的,i 和 j 又都被 id子串覆盖,所以 i 点的回文长度 应该不小于 j 点。这里面有两种情况,一种是以 j 为中心的回文串的最长长度也没有超过id子串的覆盖范围,那么 i 点可以直接取 j 的长度值了;另一种是 j 的最长长度超出id子串的覆盖范围,那么由于回文串的对称性,我们可以知道 i 点至少在id串的覆盖范围内是可以确定为回文的,这时候 i 点应该取从 i 点到id串范围的长度。具体内容如下。

    我们用一个数组p在储存每一个字符为中心的最长回文串的半径,id表示待用的节点,mx表示id串最右边的边缘位置。

    举个例子

    0   1  2   3  4   5   6   7  8   9  10     

    a   c   f   c   b   a   b   c   f   c   d 

    待求点 i = 8  , id = 5  , mx = id+p[id]=10   , i+j=2*id  , j = 2 ,p[j]=2  

    这个例子中,id刚好覆盖到 i,j 两点 我们直接取p[i]=p[j]即可

    如果 str[0]=b,那么 j 的子串是[0..4], id的子串[1..9]没有完全覆盖,我们由对称性可以得出,i 点至少到 9这个位置的回文性质是可以确定的,10这个位置是否对称还不知道,故我们只能取 i 点到mx , 即p[i]=mx-i; 概括地讲那就是 p[i]=min{p[j],mx-i};这是p[i]最小值,然后再在这个基础上扩展一下即可得出正确结果。

    这里有一个问题要注意,那就是偶数型的回文串怎么办呢,并没有一个中心点。于是,我们在初始化时,在字符串中插入了一些容易区分的字符,比如‘#‘,这样一来,一个abba的回文串,变成 #a#b#b#a#,那么这个回文串变成了一个以‘#‘为中心的回文串,将奇数回文串和偶数回文串统一起来。有意思的是,增加之后,p[i]-1就等于原字符串的长度,这个可以自己画一画,很容易看出。

    

#include<stdio.h>
#include<string.h>
int p[2200010],n;
char str[2200010],s[2200010];
void init()
{
    int i,j,k;
    n=strlen(s);
    str[0]=$;
    str[1]=#;
    for(i=0;i<n;i++)
    {
        str[i*2+2]=s[i];
        str[i*2+3]=#;
    }
    n=n*2+2;
    s[n]=0;
}
void manacher()
{
    int i,j,mx=0,id=1;
    for(i=1;i<n;i++)
    {
        j=2*id-i;
        if(mx>i)
            p[i]=p[j]<(mx-i)?p[j]:(mx-i);
        else p[i]=1;
        for(;str[i+p[i]]==str[i-p[i]];p[i]++)
            ;
        if(i+p[i]>mx)
        {
            mx=i+p[i];
            id=i;
        }

    }
}
int main()
{
    int i,j,k,ans,lp,m;
    scanf("%d",&m);
    for(lp=0;lp<m;lp++)
    {
        scanf("%s",s);
        memset(p,0,sizeof(p));
        init();
        manacher();
        ans=0;
        for(i=1;i<n;i++)
            if(ans<p[i])ans=p[i];
        printf("%d\n",ans-1);
    }
}

 

虽然有循环嵌套,但这两个循环不遵循乘法原理,内层循环只扫未确定的区域,所以时间复杂度为O(N)。

酸爽!

 

最长回文子串

标签:

原文地址:http://www.cnblogs.com/puszeto/p/4375512.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!