【通俗易懂搞算法】一篇文章弄懂Manacher算法

manacher算法解决的问题

  1. 求解最长回文子串的长度
  2. 大部分回文问题都可以用manacher算法
  3. 将求解的时间复杂度降到O(N)

回文 最长回文子串

注:此处是根据我的理解进行讲解,需要准确的定义,自行进行搜索,我的讲解只是为了让你们更好的理解

  • 回文

​ 字符串正着读反着读都是它本身,或者可以看作它是关于某一个轴对称。

​ 比如"123321" “aba” 就是回文。

  • 最长回文子串

    1. 一个字符串的子串(包括字符串本身),子串要求连续
    2. 是回文
    3. 最长

例:str为"abc112320de1"
根据最长回文子串的定义可以看出,"11"和"232"为回文子串,而“232”最长,所以“232”为最长回文子串,而"12321"不是,因为他是不连续的,也就不是str的子串。

最长回文子串解法

解法1.0

根据回文的定义我们可以想到通用的解法,就是能不能对字符串先进行遍历,然后看当前字符的左右字符是否相等,如果不相等,就直接返回当前回文长度,最开始的回文长度为1,如果相等回文子串长度加1,然后继续向左右两边扩充,不断反复,直到左右字符不等,或者到达字符串左右边界。根据这个想法,我们对上面的str字符串进行该操作。
在这里插入图片描述

通过该操作,我们似乎可以得到最长回文子串,但是,这种做法,存在一个问题,就是他忽略了回文长度为偶数的回文!!显然,假如回文是偶数的话,我们不能采取这种做法。那我们就需要对上面的想法进行改进,也就是解法2.0.

解法2.0

在原来的字符串的每个字符左右两边填充特殊字符,然后按照解法1.0的步骤求出每个字符的回文长度,然后取出最大的回文字符长度除以2,得到最终的最长回文子串长度,还是以上面的str字符为例,进行讲解。

在这里插入图片描述

我们可以看出,这样的做法对于偶数回文“11”,奇数回文“232”都求出来了,这里的除以2是向下取整不然结果长度会多1个。

理解了这种做法,我们来深入研究一下一些问题。

这里插入的特殊字符有限制吗?

答案是没有,这里的特殊字符没有什么限制,没有说非要是字符串中不存在的字符。这里你们可以看一下你们加了特殊字符,在求原来字符串的各个字符的回文长度的时候,这些特殊字符对于回文有没有影响?答案显而易见,是没有的,所以这里的特殊字符是没有限制的。

​ 这种结果的时间复杂度是多少?

​ 答案是O(N^ 2),为什么呢?这里不太好解释,我只能讲解个大概,求某一个位置的回文长度,他的往两边扩充要么扩充到左边界停,要么扩充到右边界停,字符串从左边界到字符串中间,字符串中间到字符串右边,扩充次数是个等差数列,所以时间复杂度是O( N^2 ), 假如有大佬能够讲解清楚,可以在评论区留言。

在这里插入图片描述

这里我们就引出了Manacher算法,因为它能将时间复杂度缩小到O(N)!!!假如你学过kmp算法,那么对于Manacher算法,你会觉得学起来很轻松,因为它借鉴了kmp算法的next数组进行加速,最终达到O(N)的时间复杂度。

Manacher算法

Manacher算法有四个概念需要理解分别是回文半径、回文直径,回文半径数组,之前扩的所有位置中所到达的最右回文右边界®和取得更远边界的中心点的位置©,这些概念先大致了解,等后面在详细讲解。

回文半径、回文直径

当你求解一个字符的回文字符串长度时,这个回文字符串的长度就是就是回文直径,而对这个回文字符串长度除2向上取整就是回文半径,以上面的str的回文子串“121”为例子,“121”的回文直径是3,回文半径就是2,也就是字符串的“21”或者“12”的长度。

回文半径数组

对于要求字符串的每个字符左右两边填充特殊字符,然后从左往右求每个字符的回文半径放入一个数组中,类似于kmp算法的next数组。

之前扩的所有位置中所到达的最右回文右边界®

这里我用一个例子进行讲解,文字难以表达,你从左往右看我推的过程就能看明白,假如不懂,可以在评论区留言。该值是int类型,在图中我用变量n表示。

在这里插入图片描述

取得更远边界的中心点的位置©

int类型,初始值为-1,中心点位置是随着R的值变化的,R变化则C也变化,R不变C也不变,这两个概念有点抽象,因为我们都不知道这两个概念有什么用,先大致的看一下,等后面用到了,自然就明白了。

在这里插入图片描述

Manacher算法优化情形

​ 这里的话,不要想太复杂,Manacher算法的思路还是跟解法2.0一样,但是它是为了提速,那么它提速就会有要求,就类似kmp算法,next数组里的数是-1,自然没有提速的办法,只能暴力扩充,看它的回文长度是多少,进而更新其他参数,直到满足提速要求,进行提速。

(1)当来到某个点的时候,这个点没有在最右回文右边界里,暴力扩充,不存在优化。

在这里插入图片描述

以第一个字符“#”为例子,此时最右回文右边界是-1,而中心点位置为0,此时,当前点位置不在最右回文右边界里,故直接暴力扩充。

(2)当所处点在最右回文右边界里,则存在优化。

假如所处点在最右回文右边界里,则说明当前点在中心点到最右回文右边界的内部。当前点可能跟中心点重合。

在这里插入图片描述

后续简写字母说明:

i :当前所在位置

i‘ :i关于中心点C对称位置

R:最右回文右边界

L:R关于中心点C对称位置

C:中心点的位置

按照 i‘ 的 回文状况对(2)大情形进行划分。

(2-1) i’ 自己的回文区域在L-R之间

在这里插入图片描述

此时i的回文半径跟i’的回文半径相同。

这个地方很好理解,因为L-R首先是关于C对称的,i跟i‘ 是对称的,那么i的回文半径不就跟i’的一样啦。

(2-2) i’ 自己的回文区域有一部分在L-R区域之外

在这里插入图片描述

此时i的回文半径就是i到R。

这里看图就能看明白,我就不多赘述了,总而言之,还是因为对称。

(2-3) i’ 自己的回文区域与L压线

在这里插入图片描述

此时i的回文半径只能说至少的i‘的回文半径,具体多少,还要继续扩,以上图为例,i的”abcdcba“子串肯定是回文子串,但是需要看k和?是否相等,如果相等,回文半径+1,不相等则当前大小就是回文半径。

Manacher算法优化情形总结

(1)当来到某个点的时候,这个点没有在最右回文右边界里,暴力扩充,不存在优化。

(2)当所处点在最右回文右边界里,则存在优化。

(2-1) i’ 自己的回文区域在L-R之间,此时i的回文半径跟i’的回文半径相同。

(2-2) i’ 自己的回文区域有一部分在L-R区域之外,此时i的回文半径就是i到R。

(2-3) i’ 自己的回文区域与L压线,此时i的回文半径只能说至少的i‘的回文半径,具体多少,还要继续判断。

manacher算法代码实现
/**
 * 将字符串变为处理串 字符数组
 * @param str
 * @return char[]
 */
public static char[] manacherString(String str) {
    char[] charArr = str.toCharArray();
    char[] res = new char[str.length() * 2 + 1];
    int index = 0;
    for (int i = 0; i != res.length; i++) {
        res[i] = (i & 1) == 0 ? '#' : charArr[index++];
    }
    return res;
}

public static int maxLcpsLength(String s) {
    if(s == null || s.length() == 0) {
        return 0;
    }
    char[] str = manacherString(s);  // 1221 -->#1#2#2#1#
    int[] pArr = new int[str.length];   //回文半径数组
    int C = -1;     //中心点
    int R = -1;     //最右回文右边界,这里是回文右边界再往右的一个位置,最右有效区是R-1的位置
    int max = Integer.MIN_VALUE;    //扩出来的最大值
    for (int i = 0; i != str.length; i++) { //对每个位置求回文半径
        //对两个大情况进行分析,判断不用求的回文半径是多少,也就是加速的地方
        //Math.min(pArr[2 * C  - i],R - i)
        //2*C-i = C-(i - C)
            //pArr[2 * C  - i]  2 * C  - i就是i‘ 这个参数就是i’的回文半径。
            //R - i  i到R之间的距离
            //回文半径大于 i到r之间的距离 对应 2-2 回文半径大于L-R 取i-r
            //回文半径小于 i到r之间的距离 对应 2-1 2-3  回文半径小于L-R以及回文半径在L上 取i的回文半径
        pArr[i] = R > i ? Math.min(pArr[2 * C  - i],R - i) : 1;
        //从不要求的区域开始往外扩,看能不能扩,能就是(1) (2-3)两种情况,不能则是(2-1)(2-2)
        //这里只是让代码更加简洁,避免过多的if判断
        while (i + pArr[i] < str.length && i - pArr[i] > -1) {
            if (str[i + pArr[i]] == str[i - pArr[i]])
                pArr[i]++;
            else {
                break;
            }
        }
        if (i + pArr[i] > R) {
            R = i + pArr[i];
            C = i;
        }
        max = Math.max(max, pArr[i]);
    }
    //回文半径向上取整了,所以会多1
    return max - 1;
}

  • 27
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值