这是我参与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)之间,如下:
这样的话,我们可以确定i的回文半径就和i'的一样。
- i'的回文范围不在(L,R)之间,如下:
这种情况下,i的回文半径为i到R
- i'回文半径和L压线,如下:
这种情况下咱们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);
}
最后抠边界花了一点时间😟😟