高效的求最长回文子串的算法——Manacher

GDOI市选出了一道关于Manacher的题目,于是打算学一下,但因为网络流鸽了一阵,最近才学完,学完后表示复杂度有点迷。

Manacher算法简介

Manacher,俗称马拉车,是由一位名叫Manacher的人与1975年提出的,这个算法让求最长回文子串的复杂度从O(n^2)下降到了O(n)。

Manacher的思想

先从n方算法说起,n方算法是每一位都向两边扩展,直到不回文。这个算法只是对当前位置进行操作,而Manacher算法将前面算出的值进行记录,用于后续的处理,从而降低复杂度。

我们平时用n^2算法处理的时候就会考虑到一个问题,就是一个回文串的长度,如果是像aaa一样的长度为奇数的串,它的中心是一个字符;如果是像abba一样长度为偶数的串,它的中心在两个字符中间,这样比较难处理。所以Manacher算法会在一个字符串每两个字符中间以及字符串头尾插入‘#’(也可以是其他的,有没有出现在原字符串中其实无所谓)。
如:abcdcba会变为#a#b#c#d#c#b#a#。

这里我们引入一个数组p[],p[i]表示以i为中心的最长回文串的半径长度。我们用表格举个例子:

s#a#b#a#b#a#b#a#
p121416181614121

而更神奇的是以s[i]为中心的回文串长度就是p[i]-1。

Manacher的实现

我们需要记录的除了p数组,还有在现在已知(即之前处理过的)的所有回文串中右边界最右的那个回文串c的中心位置mid以及其长度len(记录其右边界也可)。
这里写图片描述
设我们即将处理的位置为i,i关于mid的对称点为j,那么:
如果i > mid+len-1
即i在回文串c右边界的右边,没有任何先前的数据可以利用,所以p[i]=1,直接暴力求p[i]。

如果j-p[j]+1 >= mid-len+1,
即以j为中心的最长回文串(下图红色部分)在回文串c内,则以i为中心的最长回文串(下图蓝色部分)一定也在该字符串内,那么就有p[i]=p[j],如下图。
这里写图片描述
如果j-p[j]+1 < mid-len+1,
即以j为中心的最长回文串(下图红色部分)有一部分在回文串c外,则以i为中心的最长回文串(下图蓝色部分)至少有一部分可以确定(下图绿色部分),那么我们可以直接从确定的部分拓展算出p[i],这里p[i]一定小于p[j]。
这里写图片描述

代码

上面是一个理解算法的过程,事实上并不用如此复杂的操作。另外有一个小技巧,可以在字符串最前端插入一个’$’,在字符串最末端插入一个’\0’,并以此来完成对回文串边界的判断,所以插入的可以不是上述的两个字符,但是一定不能出现在原字符串中。
那么,上代码:

#include<cstdio>
#include<cstdlib>
#include<cstring>
using namespace std;
const int MAXN=11000005;
char c[MAXN],s[MAXN*2+1];
int lenc,lens;
int p[MAXN*2+1],mid,len,maxlen;
int minn(int a,int b){return a>b?b:a;}
int maxx(int a,int b){return a>b?a:b;}
void init()
{
    lenc=strlen(c);
    s[0]='$';s[1]='#';
    lens=2;
    for (int i=0;i<lenc;i++)
    {
        s[lens++]=c[i];
        s[lens++]='#';
    }
    s[lens++]='\0';
    memset(p,0,sizeof(p));
}
int main()
{
    scanf("%s",c);
    init();
    mid=-1;len=0;maxlen=0;
    for (int i=1;i<lens;i++)
    {//用mid+len作边界,和mid+len-1差不了多少,只是为了处理方便,处理时注意一下就好了
        if (mid+len>i)p[i]=minn(mid+len-i,p[2*mid-i]);
        else p[i]=1;//这里就是上面说的内容,给p[i]附一个初值
        while (1)
        {
            if (s[i-p[i]]!=s[i+p[i]])break;
            p[i]++;
        }//虽然看上去会让算法退化但是算法优啊
        if (i+p[i]>mid+len)
        {
            mid=i;
            len=p[i];
        }//更新mid和len
        maxlen=maxx(maxlen,p[i]-1);
    }
    printf("%d",maxlen);
    return (0-0);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值