Manacher(马拉车)算法详解


       马拉车用于解决最长回文子串问题,重点是子串,而不是子序列,想了解最长回文子序列的可以看下这篇博客传送门。对于这种问题,当然最简单粗暴的方法就是暴力求解,但太暴力也不好,毕竟会TLE。所以对于求最长回文子串的问题有一种神奇的算法——马拉车算法,神奇就神奇在时间复杂度为O(n)。

       我先说一下大概思路,就是用一个Len[i]数组去存第i个位置到mx位置的长度,然后用id记录上一次操作的位置,mx标记上一次的最长子串的最右端,然后依次去递推。不太好理解(花了好几个小时才懂),现在看不懂没关系,我尽量讲的详细点。

       以hihoCoder的一道裸题为例,只要能AC就差不多懂了,题目链接:传送门

       先定义一个字符串s = "ababc",因为对于回文字符串来说有对称轴,比如说aba就是以b作为对称轴,那么当字符串长度为偶数的时候,对称轴就不是唯一的整数位了。所以我们要对原字符串

       做一个预处理,在每个字符左右都加上一个特殊字符,这里我用'#',那么处理后的字符串str就是#a#b#a#b#c#了,不管原字符串长度是奇数还是偶数,处理后的长度都变成偶数了。然后还需要在这个str的最前面加一个不同于之前的特殊字符的特殊字符,这里我用了'%'(因为在后面的while循环中在匹配字符的时候可能会越界)。这样预处理操作就完了,最后的字符串str就是%#a#b#a#b#c#了。

       首先需要明白的是,我定义的mx是上一次操作的最长回文子串的最右端,我解释一下,比如说ababa,第一次对i=2操作的时候,会计算出他的最回文子串为aba(长度为3),则在这次操作结束后mx的位置就是4,也就是aba的下一位。而我定义的id是上一次操作的位置,也就是i的位置,还以刚才的例子为例,在对i=2操作结束后,id的位置就是2。姑且先这么理解,知道他是什么就行,后面再去思考。

       Len数组里存的是第i个位置到mx位置的长度,比如说还是ababa,当i=2的时候,可以计算出aba是它此时的最长回文子串,那么mx的位置就是aba的下一位,那Len[2]存的就是mx - i了。最重要的就是Len数组,所以这点一定要弄明白,Len存的是,当前这个位置到它的最长回文子串的最右端的距离(也就是mx的位置)。在str字符串中,Len[i] = 2*Len[i] - 1(因为有特殊字符),在s字符串中Len[i] = mx - id + 1。

        知道这些后,开始进入正题,先看下核心代码。


       首先把mx初始化0,然后开始从i=1开始遍历str,当i>=mx的时候,也就是i在mx前面的时候,就让Len[i] = 1,表示在i之前没有回文串出现,所以让Len从1开始,然后先不说i<mx的情况,然后往下走到while循环,这个循环就是用来判断这个位置的最长回文子串的,就是以这个点为中心,然后依次比较左边一个和右边一个、左边第二个和右边第二个...是否相等,相等的话就让Len[i]++,就相当于这个位置到这个位置的子串的最右端的长度++。然后再比较这个点+这个点的回文长度是否超过了mx的位置,超过的话就更新一下mx的值,因为mx是此次操作的最长回文子串的最右端。

               

       然后对于i<mx的情况就稍微有点不太好理解了,参考着上图我讲一下这种情况。这是一个以id(上一次操作的位置)为中点长度为mx-my的回文串,j是以id为中点的i的对称点,因为j在之前遍历的时候就已经操作过了,所以Len[j]是已知的又因为回文串的性质可以知道Len[j]<=Len[i]的,my是mx的对称点。对于i<mx的情况还要再分情况,就是Len[j]和mx-i比较了,上图的情况是Len[j]是小于mx-i的,表示j位置的最长回文子串的长度是在id位置的最长回文子串的范围内的,还有一种情况就是Len[j]>mx-i,如下图所示,Len[j]的长度的范围超出了my。

                 

       所以我们需要对这两种情况再讨论一下,当Len[j] < mx-i的时候,表示Len[i]的长度可能不会超过mx-i,所以我们就从i的Len[2*id - i]也就是Len[mx-i]的地方开始匹配。当Len[j] > mx - i的时候,说明i位置的子串长度超过了mx,但mx以外的地方还没有遍历到,所以我们就从mx-i也就是mx的位置开始对i匹配。

       然而最后的结果就是下图这样的

                           

       用一个sum去更新最大值就行了。我觉得挺不好理解的,建议耐下心想一想,模拟一遍,有不懂的都可以评论,有不对的地方也请指出。然后上一个前面说的那道题的代码,也是Manacher的模板。


AC代码:

#include <iostream>
#include <cstdio>
#include <cstring>
#define Min(a,b) a>b?b:a
#define Max(a,b) a>b?a:b
using namespace std;
int Len[3000005];
char str[3000005],s[3000005];
int n,mx,id,len;

void init(){
    memset(str,0,sizeof(str));
    int k=0;
    str[k++] = '$';
    for(int i=0;i<len;i++){
        str[k++]='#';
        str[k++]=s[i];
    }
    str[k++]='#';
    len=k;
}

int Manacher(){
  Len[0] = 0;
  int sum = 0;
  mx = 0;
  for(int i=1;i<len;i++){
    if(i < mx) Len[i] = Min(mx - i, Len[2 * id - i]);
    else Len[i] = 1;
    while(str[i - Len[i]]== str[i + Len[i]]) Len[i]++;
    if(Len[i] + i > mx){
      mx = Len[i] + i;
      id = i;
      sum = Max(sum, Len[i]);
    }
  }
  return (sum - 1);
}

int main()
{
  scanf("%d",&n);
  while(n--){
    scanf("%s",s);
    len = strlen(s);
    init();
    int temp = Manacher();
    printf("%d\n",temp);
  }
  return 0;
}


         最后上一个手动模拟了一下午的草图。




猜你喜欢

转载自blog.csdn.net/charles_zaqdt/article/details/79747073