Manacher(马拉车)理解及例题和板子

什么是马拉车

难道是一群马拉着一辆车??(手动滑稽)
显然不是。
这是百度百科的解释:
Manachar算法主要是处理字符串中关于回文串的问题的,它可以在 O(n) 的时间处理出以字符串中每一个字符为中心的回文串半径,由于将原字符串处理成两倍长度的新串,在每两个字符之间加入一个特定的特殊字符,因此原本长度为偶数的回文串就成了以中间特殊字符为中心的奇数长度的回文串了。
简单点来说就是求出来一个字符串的最大长度的回文子串。举个栗子:
在这里插入图片描述
abab 的最长回文子串是 “aba” “bab”,长度为3;
ababa 的最长回文子串是 “ababa” ,长度为5。
可能还有一些童鞋不晓得什么叫回文。这里我再解释一下;回文就是说一个字符串正着读和倒着读一摸一样。也可以说字符串为中心对称。例如 “aba” “abccba” 。实在理解不了的就问度娘吧 。
在这里插入图片描述
当然,用其他的方法也可以达到和马拉车一样的效果,但是时间上有限制,并且字符串长度比较大的话,就只能用马拉车了(毕竟比较快 )。

马拉车实现过程

1、字符之间插入特殊字符

我们知道,字符串的长度有奇偶之分。如果是奇数的话,例如:“aba” 的回文中心(其实就是对称中心)为 “b”,如果是偶数的话,例如:“abba”,的回文中心为 “bb” 中间的空白部分。这样的话对称轴 就不唯一了。那么怎么解决呢??
在这里插入图片描述如果我们在字符串中的每一个字符后面 都加一个特殊符号,例如 “#”,然后在字符串开头再加一个。那么就让字符串变成奇数了。因为如果字符串长度为n的话, “#” 的数量为n+1
2*n+1 为奇数。
为了更加直观,再举一个例子吧:在这里插入图片描述
原字符串为 “ababa” 长度为5,奇数。
处理后的字符串为“ #a#b#a#b#a# ” 长度变为11,奇数。

原字符串为“abba” ,长度为4,偶数。
处理后的字符串为 “#a#b#b#a#” 长度为9,奇数。
(信不过的可以数一下)
这样就不用考虑奇偶问题了。因为处理后的字符串恒为奇数。

2、计算半径数组 p

p 数组是我们新建的一个与处理后的数组 s 等长度的数组。我们用p[i]来存储 以s[i]为中心的回文半径。(不包括p[i]自身)
例如“#a#b#a#” 当s[i]为“b”时,即 i = 3时 回文半径为 3
为了在搜索回文子串时避免总是判断是否越界,我们在 ss 的首尾两端加上两个不同的特殊字符,保证对回文半径判断没有影响。例如前面加“$"后面加”#“(当然也可以加任意和前面不一样的)此时字符串”#a#b#a#“
变为” $#a#b#a## “
我们求出的最长半径其实就是原数组的最长回文串长度。
知道了基本思想,下面可以看一道例题,然后再根据代码来讲解一下具体用法和实现过程。
在这里插入图片描述

例题及代码实现。

题目:最长回文
给出一个只由小写英文字符a,b,c…y,z组成的字符串S,求S中最长回文串的长度.
回文就是正反读都是一样的字符串,如aba, abba等
Input
输入有多组case,不超过120组,每组输入为一行小写英文字符a,b,c…y,z组成的字符串S
两组case之间由空行隔开(该空行不用处理)
字符串长度len <= 110000
Output
每一行一个整数x,对应一组case,表示该组case的字符串中所包含的最长回文长度.
Sample Input
aaaa

abab
Sample Output
4
3
汉字应该都看得懂。
接下来代码奉上:

#include<iostream>
#include<stdio.h>
#include<string.h>
#include<algorithm>
#include<math.h>
using namespace std;

const int maxn=3e5;
char s[maxn],str[maxn];
int len1,len2,p[maxn],ans;

void init() //把原数组进行处理 
{
    str[0]='$'; //字符串开头加上特殊字符
    str[1]='#';
    for(int i=0; i<len1; i++)
    {
        str[i*2+2]=s[i];
        str[i*2+3]='#';
    }
    len2=len1*2+2;
    str[len2]='#';  //字符串末尾也加上特殊字符(和开头不同的)
}

void manacher()  //正片开始,马拉车
{
    int id=0,mx=0; //id表示回文子串的中心位置,mx表示回文子串的末尾位置。
    for(int i=1; i<len2; i++)
    {
        if(mx>i)
            p[i]=min(p[2*id-i],mx-i); //这一步是马拉车的关键部分,后面我在重点讲解。
        else
            p[i]=1;
        for(; str[i+p[i]]==str[i-p[i]]; p[i]++); //向两边扩展,匹配。
        if(p[i]+i>mx)
        {
            mx=p[i]+i;
            id=i;
        }
    }
}

int main()
{
    while(scanf("%s",s)!=EOF)
    {
        len1=strlen(s);
        init();
        manacher();
        ans=0;
        for(int i=0; i<len2; i++)
        {
            ans=max(p[i],ans);
        }
        cout<<ans-1<<endl;
    }
    return 0;
}

在这里插入图片描述
这个部分很多人都会不明白,我在详细讲解一下。
我们用图示来理解这个公式,如下图:
在这里插入图片描述
在图中,mx关于id对称,i,j,也关于id 对称。
那么,如果 mx 在 i 的右边,则我们可以通过已经计算出的 p[j] 来计算 p[i],其中 j 与 i 的中心点为 id。
因为我们知道 2*id-i 是 j 点。又因为 i 与 j 对称 mx 也关于id 对称。我们完全可以用 j 代替 i 。我们来比较 mx-i 与 p[i]的大小。
如果 mx-i 大于 p[i] ( 即 mx-i >s[i]的回文半径。)但是我们只确定到 mx 处该字符串回文,大于mx的部分不确定,所以我们只能取较小值p[i],然后再后续判断。
同理 mx-i小于p[i]时,也是如此推理。

最后总结

马拉车算法最关键最难懂的地方就是那个公式。那里是一个优化的地方,也是马拉车的核心。其他地方就相对于好理解很多了。另外记得多看几遍以免在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/sugarsong/article/details/107563386