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]存的就是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的模板。

#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(){  
    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--){  
    memset(str,0,sizeof(str));
    scanf("%s",s);  
    len = strlen(s);  
    init();  
    int temp = Manacher();  
    printf("%d\n",temp);  
  }  
  return 0;  
}  
  • 37
    点赞
  • 96
    收藏
    觉得还不错? 一键收藏
  • 11
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值