Manacher(马拉车)算法——杨子曰算法

Manacher(马拉车)算法——杨子曰算法

马拉车?中华汉字真是博大精深……


今天我们曰的算法只能干一件事情——求一个字符串的最长回文子串(神马是回文子串就不用我说了吧)


首先我们来想一想大暴力怎么做
“我知道!枚举子串的头,枚举子串的尾,再暴力判断这个串是不是回文串,复杂度O(n3)”
杨子曰:“呃……太暴力了,能不能稍稍优化一下”
我们枚举回文字串的中心,然后向两边延伸,比较是否对称,一旦不对称了,马上换下一个字符作为中心,复杂度O(n2

BUT,这个做法有一个大的弊端,就是偶数长度回文串的中心不是一个字符,比如abccba的回文中心就在两个c之间,这样一来就变得很难枚举,So,我们来做一个非常巧妙的预处理,我们可以在每个字符的中间,加上一个奇怪的符号,比如:♂,这样无论是奇回文串,还是偶回文串就都可以以字符为中心了,比如
偶回文串:abccba会变成♂a♂b♂c♂c♂b♂a♂
奇回文串:abcba会变成♂a♂b♂c♂b♂a♂
而且这样一来还有一个好处,假设加了奇怪符号后的回文串的半径为r(r是包括中心的),那么原来这个子串的长度就是r-1,岂不是美哉!

然而我们今天的主角:马拉车也是需要做这个预处理的(之后提到的字符串默认为预处理后的

接下来我们将用线性的复杂度切掉它

黑喂狗:


我们会从左到右线性地扫描整个字符串,假设当前考虑到i的位置
先了解一下扫描过程中我们需要维护的东西:

  • p[i]:以i为中心的最长回文串的半径
  • mx:我们考虑过的最右端的位置
  • id:包含了mx这一位置字符的最长回文串中心

机智的你一定发现了,我们最后的答案就是max(p[i]),那我们如何快速求出当前的p[i]呢?

1.如果i,也就是当前这个字符在回文中心为id的回文串里(绿色表示包含了考虑过的最右端mx的回文子串,它的回文中心就是id)
Manacher(马拉车)算法——杨子曰算法_第1张图片

那么这是绿色左右关于id完全对称,也就是说i也在左边有一个对称点,我们可以用2×id-i找到,而p[2×id-i],我们已经求过了,我们已经知道了以2×id-i为中心的回文子串(用红色表示)

如果i+p[2×id-i] Manacher(马拉车)算法——杨子曰算法_第2张图片
红色部分还没有超出绿色部分,有没有发现由于对称,p[i]=p[2*id-i]

但如果i+p[2×id-i]>=mx也就是长这样(蓝色红色共同表示以2*id-i为中心的回文子串):
Manacher(马拉车)算法——杨子曰算法_第3张图片
由于对称的只有绿色部分,So只有红色部分对i来说有作用,有人说:这时候i的红色部分左右还有可能可以扩展呀!没事,那我们就暴力去扩展就行了

2.i>=mx,也就是i不在绿色里面,那之前的绿色里的信息对i一点帮助也没有,长度只好从1开始暴力地往两边判断

这样一来,我们就完美地实现了马拉车算法


这个算法的复杂度为啥时O(n)捏?

这很显然,如果我们在考虑回文中心为i的回文串时右端点时不超过mx的,那么就可以用O(1)更新,如果右端点超过了mx,或者i>=mx,我们在暴力判断超过部分时mx也随之更新,mx是单调递增的,向右移动次数不会超过n,So,总复杂度就是O(n)


打代码时注意:

  • 不要忘记每个p[i]算好时,都要和p[id]取max,更新id 和mx
  • 用‘♂’作为预处理的字符纯属娱乐,真正使用可能会错,所以使用一些正常的字符,如:‘#’(只要没有在原串中出现过就行)

OK,完事


c++代码:

int manacher(char *s){
    int ans=0;
    int n=strlen(s);
    string t="#";
    for (int i=0;i<n;i++){
        t+=s[i];
        t+='#';
    }
    t+='@';//此时t为预处理后的字符串
    p[0]=1;
    int mx=0,id=0;
    n=t.length();
    for (int i=1;i<n;i++){
        if (i<mx) p[i]=min(p[2*id-i], mx-i);
        else p[i]=1;
        while(t[i-p[i]]==t[i+p[i]]) p[i]++;
        if (mx<i+p[i]) id=i,mx=i+p[i];
        ans=max(ans,p[i]-1);
    }
    return ans;
}

于HG机房

你可能感兴趣的:(变态的算法,算法与数据结构)