Manacher算法详解

Manacher算法详解

版权声明:部分内容及图片参考自简书


本篇随笔详解一下信息学奥林匹克竞赛中字符串问题的一种较为常用的算法——\(Manacher\)算法。也被戏称为“马拉车算法”。这种算法常被应用于求解最长回文子串长度的问题。对于字符串的基本处理及最长回文子串概念的相关知识,属于基础语法和概念知识的范畴。本随笔作为一篇探讨算法本身的随笔,不再对此进行过多介绍,请有需要的读者自行查阅相关资料。


例题及分析

——\(POJ\,\,3974\,\, Palindrome\)

POJ链接

题解博客链接

题目大意:给定一个长度为\(N\)的字符串,求其最长回文子串长度。

题目分析:算法:哈希+二分(复杂度为\(O(NlogN)\)),\(Manacher\)算法,(复杂度\(O(N)\))。

哈希+二分

我们可以用\(O(N)\)的时间处理出字符串的每个前缀的哈希值,这样,我们就可以通过子串哈希求出每一个子串的哈希值。根据回文串的性质,我们可以枚举每一个数为中心,用\(O(logN)\)的时间求出最长的回文子串,然后取最大值即可(总时间复杂度为\(O(NlogN)\)).

\(Manacher\)算法

————


Manacher算法的相关概念

这里需要先提一嘴的是,为了方便处理,在进行\(Manacher\)算法前,我们输入的字符串需要被处理成中间“夹带”字符串的形式。即:假如我们输入的字符串为:\(aabba\),就会被处理成\(\#a\#a\#b\#b\#a\#\)

1、最大回文半径

上图理解:

所谓最大回文半径,可以这样理解:就是针对于每一个位置(注意,这里的#号也算是位置!),我们向两侧扩展,求出以这个位置为中心的最长回文子串,那么这个位置的最大回文半径就是这个最长回文子串的长度的一半(因为是半径)。

这里需要注意:如果是奇数回文子串的话,这个最大回文半径还需要加上这个中心位置本身!

2、最右回文右边界

我们已经知道,对于每个位置,都可以求出一个最大回文半径。(即便这个最大回文半径就是1),那么,对于每个位置扩展出来的最大回文子串,都有一个右边界(这个应该很好理解)。那么,对于当前已经枚举的所有位置中,得出的最右侧的右边界就是这个回文子串的“最右回文右边界”(这是一个顾名思义进行理解的过程)。

还是上图理解:

3、最右回文右边界的对称中心

就是上面讲过的“最右回文右边界”的对称中心(emm...)

继续上图理解:


Manacher算法的实现原理

因为\(Manacher\)算法的时间复杂度是线性的(\(O(N)\)),所以我们在思考其实现原理的时候也需要考虑如何线性地扫描求解一个最长回文子串的问题。

假设现在已经扫描到某一个位置,那么这个位置对应的最右回文右边界和其对称中心都是已知的。那么现在我们要继续往下遍历,就会出现这么几种情况:

第一种:下一个位置\(i\)的位置在最右回文右边界左侧,且在其对称中心右侧。

那么根据当前的最右回文右边界的对称中心(有了最右回文右边界和其对称中心,我们就可以知道这个回文子串究竟是哪部分),我们就可以把这个位置\(i\)关于这个对称中心做对称,当然,这个对称之后的位置(通过计算可得是\(mid\times 2-i\))的最长回文半径是不会大于当前位置的最大回文半径的,那么就把当前位置的最大回文半径更新成对称位置的回文半径。然后继续尝试扩展,看看能不能更大,持续更新最右回文右边界和其对称中心。

第二种:下一个位置\(i\)的位置在最右回文右边界右侧

这种情况我们就无能为力了,因为对称过去已经超出了我们已知的范围,这个时候就需要从1开始遍历,更新最右回文右边界,和其对称中心。

总的来说,\(Manacher\)算法的精髓就在于对“扩展”最长回文子串的一种别致的思考。这种简化看似平平无奇(甚至我第一次看还觉得比二分慢),但是在数据量较大的情况下堪称利器。


Manacher算法的代码实现

下面给出另一道模板题:

洛谷 P3805

(裸的马拉车)

的代码:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxl=51000100;
char a[maxl];
int rad[maxl];
char s[maxl<<1];
int len,ans=1;
void init()
{
    s[0]=s[1]='#';
    for(int i=0;i<len;i++)
    {
        s[i*2+2]=a[i];
        s[i*2+3]='#';
    }
    len=len*2+2;
}
void manacher()
{
    int mr=0,mid;
    for(int i=1;i<len;i++)//线性枚举
    {
        if(i<mr)
            rad[i]=min(rad[(mid<<1)-i],rad[mid]+mid-i);//如果是第一种情况,那么先给rad[i]赋上此时合法最大值。
        else
            rad[i]=1;//如果是第二种情况,什么也不知道,只能赋1
        for(;s[i+rad[i]]==s[i-rad[i]];rad[i]++)
            if(i+rad[i]>mr)//开始进行扩展,逐一增加rad[i],同时更新最右回文右边界和其对称中心
            {
                mr=i+rad[i];
                mid=i;
            }
    }
}
int main()
{
    scanf("%s",a);
    len=strlen(a);
    init();//预处理部分,将原串处理成中间夹带'#'的串,便于操作。
    manacher();//开始拉车...
    for(int i=0;i<len;i++)
        ans=max(ans,rad[i]);//更新答案。
    printf("%d\n",ans-1);//注意,答案要减去1.
    return 0;
}
posted @ 2019-11-25 20:43  Seaway-Fu  阅读(356)  评论(0编辑  收藏  举报