Manacher算法(一起来看马拉车)

865 阅读3分钟

这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战

Manacher算法是用来解决字符串中寻找最长回文子串的问题。在讲解Manacher算法之前,我们先来了解几个概念:

  • 回文直径:以一个字符为中心所扩出来的回文字符串的长度。 如"abcba",第一个a的回文直径就是1,因为它没办法往两边扩,所以以它为中心的最长回文子串是自己。c的回文直径就是5,因为以它为中心的最长回文字符串就是"abcba"。
  • 回文半径:以一个字符为中心所扩出来的回文字符串左/右边界到中心字符的长度。
  • 回文半径数组:储存每个字符的回文半径。
  • 最右回文右边界:一个记录着回文子串已经到达了的最右边界。 我们将最右回文右边界记为R,初始值为-1。还是举"abcba"为例,到达第一个a时以其为中心的最长回文子串是它本身,所以最右回文右边界可更新为0,到达c的时候会更新为4,之后都不会更新了,因为以第二个b和以第二个a为中心的最长回文子串的右边界小于等于4。
  • 最右回文右边界中心:对应着第一次到达最右回文右边界的回文字符串的中心,记为C,同样初始化为-1。

有了这些概念之后,我们将目前处于字符串的位置记为i,则可以分为以下两种大情况:

(1)i不在最右回文右边界内

对于这种情况我们没办法加速,只能继续往外扩

(2)i在最右回文右边界内 这种大情况又可以细分为以下几种情况,记i关于最右回文右边界中心的对称点为i',L为最左回文左边界:

  • i'扩的区域在(L,R)之间,如下:

image.png 这样的话,我们可以确定i的回文半径就和i'的一样。

  • i'的回文范围不在(L,R)之间,如下:

image.png 这种情况下,i的回文半径为i到R

  • i'回文半径和L压线,如下:

image.png 这种情况下咱们i的回文半径还没尘埃落定,还得继续往外扩。

PS:上面举的例子都是奇回文的例子,偶回文就不适用,难道我们还要分奇数和偶数的情况吗?只需要对我们的字符串进行一下预处理,在每个字符前加上一个字符(这里用了#,用其它字符也可以哒)就可以抹平奇偶的差异啦。格式字符串的方法如下:

function formatString(str0){
    let charArr=str0.split('');
    let res=[];
    let index=0;
    for(let i=0;i<(charArr.length*2+1);i++){
        res[i]=(i%2==0)?'#':charArr[index++];
    }
    return res;
}

接下来写一个马拉车算法的代码:

function maxLcp(s){
    if(s.length==0)return 0;
    let charArr=formatString(s);//先预处理一下字符串
    let pArr=[];
    let C=-1;//最右回文右边界中心
    let R=-1;//最右回文右边界
    let resCenter=-1;//保存最长回文子串的中心,方便返回子串
    let max=Number.MIN_VALUE;//最长回文子串的半径
    for(let i=0;i<charArr.length;i++){
    //R<i是i不在最右回文右边界内的情况,R>i就是i在最右回文右边界内的情况
    //如果是在最右回文右边界的情况,那么它的瓶颈就是(pArr[2*C-i],R-i中更小的那一个
        pArr[i]=R>i?Math.min(pArr[2*C-i],R-i):1;
    //这是为了精简代码,把几种情况合并写了,都统一往外扩
    while(i+pArr[i]<charArr.length&&i-pArr[i]>-1){
        if(charArr[i+pArr[i]]==charArr[i-pArr[i]]){
            pArr[i]++;
        }
        else {break;}
    }
    //更新最右回文右边界
    if(i+pArr[i]>R){
        R=i+pArr[i];
        C=i;
    }
    //更新最大回文半径
    if(pArr[i]>max){
        max=pArr[i];
        resCenter=i;
    }
}
   // console.log(max);
   // console.log(resCenter);
   //获取最长回文子串起始位置
    let start=Math.floor((resCenter-(max-1))/2);
   // console.log(start);
    return s.substring(start,start+max-1);
}

最后抠边界花了一点时间😟😟