字符串-Manacher‘s Algorithm马拉车算法

字符串的另一个算法:Manacher‘s Algorithm,是一个名为Manacher的人发明的一个关于最长回文子串的算法,称为Manacher算法,俗称马拉车算法。
最长回文串(Longest palindromic substring)是指的在一个字符串中查找最长的回文子串,回文串(palindromic string)是指这个字符串无论从左读还是从右读,所读的顺序是一样的;也就是说,回文串是左右对称的。例如:level、ana等等。

最简单的方法就是暴力穷举法,从字符串的第一个字符到最后一个字符,分别对从该字符开始的所有长度的子串进行判断是否是回文串(该子串的第一个和最后一个字符是否相等、第2个和倒数第2个是否相等。。),代码大致如下:
判断一个子串是否是回文串的代码:

    mid = len(s) / 2
    last = len(s) - 1
    for (i= 0; i < mid; i++) {
        if (s[i] != s[last - i])  return false;
        }

主程序代码:

  for( i = 0; i < last; i++) { 
       for (j= i + 1; j <= last; j++) { 
          //调用判断子串是否是回文串的函数。。。。。。 
               longest = s[i:j+1]           } 
  }   

显然,这个程序的时间复杂度是O(n3),效率很低。

Manacher在1975年在ACM上发表了一篇论文:Manacher, Glenn (1975), “A new linear-time “on-line” algorithm for finding the smallest initial palindrome of a string”, Journal of the ACM, 22 (3): 346–351, doi:10.1145/321892.321896,给出了Manacher算法,这个算法可以在线性时间复杂度的情况下找出最长回文子串。
关于Manacher和他的算法,我在wikipedia上查到了相关的介绍,还有他的这篇文章的链接,可惜要从ACM上下载他的这篇文章需要15$$(ACM的会员是10刀)。

在这个算法中,首先要对字符串做一个简单处理,把它的长度统一变成奇数,以方便后续处理。因为偶数长度的回文“deed”和奇数长度的回文“level”处理起来不太一样,所以最好是都统一成奇数长度,方法是给原字符串中每一个字符的左右都加上一个特殊字符如“#”:
“deed” --> “#d#e#e#d#”,“level” --> “#l#e#v#e#l#”
道理很简单,无论原字符串长度是奇数偶数,首尾都各加了一个“#”,另外,原长度是偶数的话,它们之间有奇数个空,可以加入奇数个“#”。同理原长度是奇数的话,它们之间有偶数个空可以加入偶数个“#”,这样就都变成了奇数长度,而且这样做并不改变原字符串中的回文。

当字符串长度都变为奇数时,回文串就会有一个中心对称轴,就是回文中间那个字符,他的两边都是对称相等的,这个对称相等的两边的字符长度也可以叫做半径。

第二步就是要对这个长度变为奇数的字符串s[],生成一个对应的数组p[ ]。对于每一个字符s[i],对应的p[i]是一个整数值,这个值代表在字符串s中,以字符s[i]为中心,它的回文半径的长度,如果p[i]=1,表示回文只是s[i]自己。
看一个例子:
在这里插入图片描述
原串是“abbabb”,s[]是加入“#”后的串“#a#b#b#a#b#b#”,则p[]=“1212521612321”
显然,p[i]中最大的是p[8]=6,对应的回文中心是“a”,回文半径长度6,对应最长的回文是“#b#b#a#b#b#”,对应的原字符串回文是“bbabb”,最大回文长度为5。

可以推导出,当p[i]为最大回文半径时,对应的原字符串中最大回文长度为( p[i]-1 )。
显然,如果有了数组p[],只要找出最大的p[i],那么原串中的最大回文长度就是p[i]-1。

那么,如何计算出数组p[]呢?这是这个算法的关键。
假设,有一个字符串s[],我们现在要计算p[i],p[i]以前的每一个p值都已经计算出来了。而且在之前的计算过程中,对于第id个字符,它的回文子串最远达到了mx这个位置。
在这里,我们要分为“mx>i” 和 “mx<i” 两种情况,并且在“mx>i”这种情况下,还要再分为两种情况分别考虑。

2.1 当(mx>i)时,再分为两种情况

当 mx>i时,则 p[i]=MIN( p[2*id-i], mx-i)。写成代码就是:

if( mx > i)
    p[i]=MIN( p[2*id-i], mx-i)

首先,以id为中心,回文对称半径时mx,id左边长度为mx个字符(含第id个字符)和右边长度为mx个字符是对称相等的。如果 mx>i,i也在这个回文半径内,那么i在id的左边一定有一个对称点,假设这个对称点的坐标为j,则 id - j = i - id,整理一下就是:j = 2*id -i 。由于以id为中心,长度为mx的左右两边对称相等。同理,左边以j为中心的小回文串,一定和右边 以i为中心的小回文串相同,按理说,p[i]=p[j],但这时p[i]的计算还要分两种情况考虑:

2.1.1 第一种情况

第一种情况如下图,以i为中心的小回文半径长度小,它的回文串没有超过mx,也就是 p[i]<mx,蓝色的小回文串包含在红色的大回文串中,这种情况下p[i]=p[j]。
在这里插入图片描述

2.1.2 第二种情况

如下图,以id(字符‘h’)为中心的回文串半径是9:abcbaefg h gfeabcba。i的位置是指向右边的字符‘c’,mx是指向最右边的字符‘a’,此时mx>i。i的对称点j是指向左边的字符‘c’。在第二种情况中,虽然以id为中心,半径是9的回文串中左右红框内的字符串是对称相等的,但以j为中心的小回文和以i为中心的小回文串并不一样。以j为中心的小回文串半径是5,以i为中心的小回文串半径是3,p[i]≠p[j],如果i+p[j]就超出了mx的范围,也就是mx-i<p[j]。根据以id为中心,最远到mx的回文串左右对称相等的性质可知,以i为中心的小回文串半径最多到mx位置,也就是这种情况下,p[i]=mx-i。
在这里插入图片描述
综合上面两种情况,p[i]要么等于p[j],要么等于mx-i。写成代码就是:

if( mx > i)
    p[i]=MIN( p[2*id-i], mx-i)
2.2 当(mx<i)时

当i>mx时,i已经超出了已处理过的回文串的最远位置。这种情况下是无法用i的对称点j的p[j]来推导p[i]的,因为他们不在一个大的回文串范围内,所以他们并不对称相等。
这时,只能先让p[i]=1,然后通过一个while循环,依次判断i两边的每一对字符是否对称相等,如果是则p[i]++,直到不相等。这样就完成了p[i]的计算了。代码如下:

         if (mx>i) p[i]=min(p[2*id-i],mx-i);
           else p[i]=1;
         while (s[i+p[i]]==s[i-p[i]]) p[i]++;

另外,当计算好了当前的p[i]后,还要检查一下是否要更新mx和id的值,也就是说,如果新计算出来的回文半径p[i]大于原来记录的最远距离mx,就用p[i]代替原来的mx,同时用i代替原来的id,代码如下:

        if (mx < i + p[i]) {
            mx = i + p[i];
            id = i;
        }

完整代码如下:

Manacher(string s) {
    // ----在原串中插入 '#'----------------
    string t = "$#";
    for (int i = 0; i < s.size(); ++i) {
        t += s[i];
        t += "#";
    }
    // ----处理串 t ---------------
    vector<int> p(t.size(), 0);
    int mx = 0, id = 0;
    for (int i = 1; i < t.size(); ++i) {
        p[i] = mx > i ? min(p[2 * id - i], mx - i) : 1;
        while (t[i + p[i]] == t[i - p[i]]) ++p[i];
        if (mx < i + p[i]) {
            mx = i + p[i];
            id = i;
        }
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值