算法进阶之manacher算法 (求最长回文)

来源:互联网 发布:淘宝无营业执照 编辑:程序博客网 时间:2024/06/06 06:33

前几天bestcode做到一道字符串的题目,需要O(n)的回文,正好看到网上的manacher算法,于是来学习一发


进入正题:

manacher算法


用法:一般用于求一个字符串的最大回文,操作过程中会记录以每个点为中心的回文半径,可用来进行其他操作

时间复杂度:O(n)

空间复杂度:记录字符串2*n,半径数组2*n


内容:

记p[i]为以i为中心的回文半径(aba中以b为中心的回文半径为2,那么如果是偶数个数的回文呢,比如abba?这里先讨论奇数,偶数后面再说,简单说就是用字符填充变成奇数)

因此以下的部分讨论的都是奇数个数的回文>>>>>>>>>>>>>>

那么无可避免先要for一遍,for(j=0;j<n;i++) 来求每个p[j],下面就是对p[j](令j=i+k)的求解

分析:假设已经求出了i+k前面的0~i+k-1的p,现在要求p[i+k],

 那么首先根据前面的点求半径的时候有没有遍历到了i+k这个点可分为两大类:

一、 前面的点求半径的时候还没有遍历到了i+k这个点,即p[i]+i<i+k

(设i为前i+k个点中半径时遍历到最靠近字符串后面的点,不是回文最长!)

那么此时p[i+k]=1;

这时候说明此时的i+k还没找过半径,需要从头开始找while(s[i+k+p[i+k]]==s[i+k-p[i+k]]) p[i+k]++;

二、 前面的点求半径时已经遍历到了i+k这个点,即p[i]+i>=i+k
(设i为前i+k个点中半径时遍历到最靠近字符串后面的点,不是回文最长!)

这时候i+k前面有关于i对称的i-k,所以算i+k的回文半径时不需要从第一个开始找,那么是从p[i-k]个开始找吗?

答案是不一定的!

下面借用网上的图片来分析一下可能出现的两种情况:


<1>p[i]-k<p[i-k](此时p[i-k]>=p[i+k]),下面是结合图片的分析


首先解释一下图,图中的红线为i的回文半径,黑线为i求回文半径时遍历过的点,深蓝线是i-k的回文半径,那么此时i+k的回文半径一定是橙线,为什么呢?

首先橙线部分肯定是i+k的半径的一部分,由于对称性可以得到与左边i-k的橙线部分相同,

那么i+k的半径可能大于橙线部分吗?

假设i+k的回文半径大于橙线,如下图中橙线+紫线,那么由于对称性i-k也必定含有紫线部分,那么此时p[i]就不再是原来的p[i]了,而是如下图的黑线+紫线,这个与之前矛盾,所以不可能存在


即p[i+k]=p[i]-k;

<2>p[i]-k>=p[i-k](此时p[i-k]<=p[i+k]),下面是结合图片的分析


图中的红线为i的回文半径,黑线为i求回文半径时遍历过的点,深蓝线是i-k的回文半径,首先由于对称性i+k的回文半径一定包含蓝线部分,与此同时i+k的右边可能还有字符能组成更长的回文,最后形成图中的橙线部分,因此此时P[i+k]>=p[i-k],因此可以先赋值p[i+k]=p[i-k],然后while(s[i+k+p[i+k]]==s[i+k-p[i+k]]) p[i+k]++;继续往后面找。


于是由上可以总结出p[j]的求法:

void query_p(int n)                             //n为字符串的长度{    int id = 0;                                 //id为上述i    p[0] = 1;    for(int j=1;j<n;j++)                        //j为上述i+k,所以k=j-id    {                       if(p[id]+id<=j) p[j]=1;                 //第一种情况        else p[j]=min(p[2*id-j],p[id]-(j-id));  //第二种情况的两种小情况        while(s[j+p[j]]==s[j-p[j]]) p[j]++;     //继续往后找        if(p[j]+j>p[id]+id) id=j;               //更新最靠近字符串后面的点    }}

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<到这里为止都是把回文当做奇数个来算的,那么如果出现偶数个怎么办呢,具体做法是利用字符填充,字符可以用字符串中肯定不会出现的字符来填充,具体做法是:

假设字符串中只有字母,那么可以利用特殊符号填充,填充方法是在每个字符的前面插入一个一样的字符

比如填充字符时#时,原字符串是abba,填充后的字符变为#a#b#b#a#,然后就可以了,至于字母的原始回文半径就是现在的p[i]/2+1,填充字符不会影响结果。

不过这里需要注意的是:为了避免数组越界需要在填充后在字符再处理一下如上述*#a#b#b#a#'\0',这样从第一个字母开始遍历的时候就不会出现问题了

这里贴一个填充后的求最长回文的代码:

int for_max(int n)                             //n为原字符串的长度{    for(int j=n;j>=0;j--)                       //字符填充    {        s[j*2+2]=s[j];        s[j*2+1]='#';    }    s[0]='$';                                   //防止数组越界填充第一个字符和最后一个字符为‘$’和‘/0’    int id = 0;                                 //id为上述i    int maxlen = 0;    p[0] = 1;    for(int j=2;j<2*n+2;j++)                        //j为上述i+k,所以k=j-id    {        if(p[id]+id<=j) p[j]=1;                 //第一种情况        else p[j]=min(p[2*id-j],p[id]-(j-id));  //第二种情况的两种小情况        while(s[j+p[j]]==s[j-p[j]]) p[j]++;     //继续往后找        if(p[j]+j>p[id]+id) id=j;               //更新最靠近字符串后面的点        maxlen=max(maxlen,p[j]-1);              //更新最长回文    }    return maxlen;}



具体题目可以看HDU3068:题意大概为给一个字符串,求最长回是多少

http://acm.hdu.edu.cn/showproblem.php?pid=3068

代码如下:

#include <iostream>#include <cstdio>#include <cstring>#include <queue>#include <algorithm>#include<vector>#pragma comment(linker,"/STACK:1024000000,1024000000")using namespace std;const int maxn = 2.2e5+5;                         char s[maxn*2];                                 //2倍数组int p[maxn*2];int for_max(int n)                             //n为原字符串的长度{    for(int j=n;j>=0;j--)                       //字符填充    {        s[j*2+2]=s[j];        s[j*2+1]='#';    }    s[0]='$';                                   //防止数组越界填充第一个字符和最后一个字符为‘$’和‘/0’    int id = 0;                                 //id为上述i    int maxlen = 0;    p[0] = 1;    for(int j=2;j<2*n+2;j++)                        //j为上述i+k,所以k=j-id    {        if(p[id]+id<=j) p[j]=1;                 //第一种情况        else p[j]=min(p[2*id-j],p[id]-(j-id));  //第二种情况的两种小情况        while(s[j+p[j]]==s[j-p[j]]) p[j]++;     //继续往后找        if(p[j]+j>p[id]+id) id=j;               //更新最靠近字符串后面的点        maxlen=max(maxlen,p[j]-1);              //更新最长回文    }    return maxlen;}int main(){    int n;    while(scanf("%s",s)!=EOF){        int m=strlen(s);<span style="white-space:pre"></span>        printf("%d\n",for_max(m));    }}


hdu5340:http://blog.csdn.net/sin_xf/article/details/47311327


1 0
原创粉丝点击