manacher算法:马拉车算法:返回字符串s的最长回文子串长度

manacher算法:马拉车算法:返回字符串s的最长回文子串长度

提示:manacher算法又是类似KMP这样需要预设信息数组的高级算法,舍弃思想,难但是很经典
据说互联网大厂的面试阶段,有很多关于回文串的考题,所以,manacher算法,一定要学透了

关于回文串的基础知识:
【1】判断链表是否是回文链表?回文结构,回文串
【2】删除字符串s中的某些字符让s成为回文串,有多少种删除方案?
上面的【2】文章,可以算是动态规划考题中变态难度级别的题目了,有兴趣看,没兴趣别看


题目

给你一个字符串s,请你返回字符串s的最长回文子串的长度max是多少?

所谓回文串,咱们之前说过了,s正念反念都相等
s的逆序=s
这种串就是回文串
回文串的左右是对称的

【链表那个章节,咱们已经学得很6了,当时叫判断链表是否回文结构?】
关于回文串的基础知识:
【1】判断链表是否是回文链表?回文结构,回文串
【2】删除字符串s中的某些字符让s成为回文串,有多少种删除方案?
上面的【2】文章,可以算是动态规划考题中变态难度级别的题目了,有兴趣看,没兴趣别看


一、审题

示例:s=1213
121就是最长的回文子串,其长度为3

s=x12213
1221就是最长的回文子串,其长度为4


暴力解o(n^2)复杂度高,要不得

如果说s的长度为奇数,好办

从s的每一个i位置,从i左右扩就完事了
比如:
在这里插入图片描述

但是s长度要是偶数呢,你没法搞,下图虚轴在粉色那还行,这样max=8
但是粉色i和橘色i扩都没法得到正确答案
在这里插入图片描述
那就这么束手就擒了吗??

填充虚轴!补上一个无关字符即可

“#”无关字符填充虚轴

在s的前面,任意俩字符中间,后面,都加上一个无关的字符“#”
你也可以随意加别的字符,没啥大不了,就是当虚轴而已
下面咱们选择加“点”
这样我画图简单,写代码随意加啥都行
在这里插入图片描述
显然22中间那个虚轴i的最长回文长度为17,max/2就是咱们要的结果8

每一个i位置,你都要扩,最惨的就是扩整个数组这么长
i有N个位置,每个位置扩N次
复杂度自然就是o(n^2)
要不得

虽然代码也能很容易写出来,但是复杂度太高

    //暴力解
    //最次,相同的字符,每一个位置i都要左右找,直到边界,这是一个等差数列的关系,自然复杂度就是o(N^2)
    //暴力寻找流程
    //arr N规模,头,每个数之间,尾都加入一个特殊字符,这个字符随意,只当虚轴
    //对arr的每个i位置,寻找到边界,然后对比左右是否相等,等就好办,不等就就停
    //得到回文长度数组backarr,长度规模2N+1,因为头尾,数中间都插入了
    //backarr全体/2,取最大值就是结果

    public static int getBackMaxLen(int[] arr){
        int[] arr2 = new int[2 * arr.length + 1];
        int index = 0;//索引arr
        arr2[0] = '#';//头
        arr2[arr2.length - 1] = '#';//尾
        for (int i = 0; i < arr.length; i++) {
            arr2[2*i+1] = arr[index++];//奇数位是arr的各个值
            arr2[2*i] = '#';//偶数位全是外字符
        }

        int max = Integer.MIN_VALUE;//准备逐个找,然后得最值
        for (int i = 0; i < arr2.length; i++) {
            int num = 1;//本身就是一个
            for (int j = i - 1, j2 = i + 1; j >= 0 && j2 < arr2.length; j--, j2++) {
                //对称位置
                if (arr2[j] == arr2[j2]) num += 2;//一下子增加俩
                else break;//如果不等,不必再继续比较了
            }
            //每一个位置得到回文num,跟max比较
            max = Math.max(max, num);
        }
        //整个数组都比完以后,需要max,除2才是arr的最大回文长度

        return max / 2;
    }

    public static void test(){
        int[] arr = {3,1,2,1,1,2,1,4};
//        System.out.println(getBackMaxLen(arr));
        char[] chars = manacherString("121");
    }

测试一把:

6

这不是重点
咱们把添加虚轴符号的代码写一下
(1)生成新的字符数组,长度是原来数组M长的:2M+1,尾部要加上
(1)每逢偶数位置0 2 4 --2M-2处,都是虚轴符号“#”
(2)每逢奇数位置1 3 5–2M-1处,copy字符串s即可
(4)别忘了2M位置也要加“#”,末尾
代码手撕一下,问题不大:

    //添加#到头,中,尾
    public static char[] manacherString(String s){
        char[] s1 = s.toCharArray();//长度M
        char[] str = new char[2 * s1.length + 1];//2M+1的规模,偶数位置全是#,奇数位置是s1
        int N = str.length;

        str[N - 1] = '#';//一头一尾
        int index = 0;//索引str的
        for (int i = 0; i < N; i++) {
            str[2*i + 1] = s1[index++];//奇数位置是str
            str[2*i] = '#';//偶数位置是虚轴符号到2M-2
        }

        return str;
    }

测试一把:

    public static void test(){
        int[] arr = {3,1,2,1,1,2,1,4};
        System.out.println(getBackMaxLen(arr));

        char[] chars = manacherString("121");
        for(Character ch:chars) System.out.print(ch+" ");
    }
# 1 # 2 # 1 #

下面manacher要用这个添加函数的


manacher马拉车算法优化到o(n)更新max结果

好,现在开始将manacher算法如何优化的
这个代码非常非精致简介,需要搞懂其中的逻辑,才能一举写出代码来,明白其中的道理

回文串的回文半径,回文区域,当前回文最右边界R,这个最右边界的回文中心C

s=x121y
添加虚轴字符处理后得到
s=#x#1#2#1#y#
在这里插入图片描述
图中,i是即将要去扩展的位置,最后能阔刀L–R范围,这个区域就是回文区域,R-L+1是回文直径
R是i=C这个回文中心的回文区域的最右边界
回文半径就是R-C+1

这些概念有了,咱们就可以利用一个思想加速寻找最长回文子串了

咱们定义一个回文半径数组pArr,它是s(加了“#”的)每一个i位置能得到的最大回文半径,
其中的max就是咱们要的答案,返回max-1
比如上面例子,max=4,原始s字符串(没加“#”的)的回文结构是121,长度为3,即max-1=4-1=3

现在咱们就只关心加了虚轴字符的s字符串(加了“#”的)
我们自然是一路,从i=0–N-1位置求pArr,在这个过程中,
有了0–i-1范围上的回文半径信息,能否利用他们加速求出i位置的回文半径? 而不要每次都从i开始使劲往外扩。

也就是类似KMP算法的next信息数组,
有了next[i-1]信息,看看i-1位置的字符是否等于y=next[i-1]处的字符,这样省掉了s中与match已经匹配过的前缀串,舍弃match串的j从0重头对比的暴力步骤!
【这KMP具体怎么搞得,请看文章:KMP算法:在字符串s中搜索匹配查找match字符串,如果能找到返回首个匹配位置i,否则返回-1】【看完好好理解一下舍弃的思想,应用到本文中的manacher算法,你就明白了,也更容易动我要讲得东西】

这种舍弃思想,我们也想借鉴到manacher算法中,争取利用0–i-1上的pArr信息,舍弃一些每次从i位置,都要从i这往外扩的暴力步骤
这种舍弃思想,我们也想借鉴到manacher算法中,争取利用0–i-1上的pArr信息,舍弃一些每次从i位置,都要从i这往外扩的暴力步骤
这种舍弃思想,我们也想借鉴到manacher算法中,争取利用0–i-1上的pArr信息,舍弃一些每次从i位置,都要从i这往外扩的暴力步骤

所以,我们要在全程额外申请一个o(n)的空间,来存pArr数组
众所周知的事情就是,算法中优化时间和空间,向来都是反比关系,你想省空间,速度就得慢,时间复杂度高,你想速度快,那必然消耗额外空间复杂度

搞懂填写回文半径数组pArr的2个大情况,3个小情况

okay,说了这么多,你知道我为啥要准备pArr数组了吧!

now,咱们来说:填写回文半径数组pArr的2个大情况,3个小情况
每次你不是要去i那收集pArr[i]信息吗:i位置为中心,能扩出的回文半径是多少?
0–i-1范围上,当初那些i位置,扩出来的回文区域最远右边界是R,其回文中心是C

情况【1】,当i>R时:你没得选,跟暴力扩展一样,直接从i往外扩
在这里插入图片描述
这就如同最开始,R=-1,C=-1,你i=0,自然i>R,第一次你没得选,你就得从i开始外扩试试能有多少是回文的

情况【2】,当i<=R时:分为3种小情况
我们在情况【2】这里,关注的是i位置,与C对称的那个位置i1,它的回文边界L1–R1是啥情况
如果C当前扩到的最左右边界是L–R的话
(1)i的对称位置i1的回文区域在C的回文区域内
即L<L1时,R1自然是<R的
这时,i1的回文半径就是i的回文半径,pArr[i]=pArr[i1]
咱们来看例子:
在这里插入图片描述
图中i的对称位置i1的回文区域L1–R1在C的回文区域L–R内,因为C能扩到R,必然C两边都是对称的
i1的回文区域在L–R内,必然i位置的回文区域L2–R2完全和L1–R1是相等的字符串,否则C就没法扩到R了
你想想是不是?
故,pArr[i]=pArr[i1]

其实,这里你能发现啥呢?咱们来到i,有了对称位置i1的pArr信息,咱们可以省掉i从头扩的步骤,这就是舍弃思想的妙处啊!

(2)i的对称位置i1的回文区域在C的回文区域外
即L1<L时
这时,i的回文半径就是R-i+1,pArr[i]=R-i+1 ,也即是i的回文区域最远只能扩到R
我们来看例子:
在这里插入图片描述
图中C的回文区域L–R左右是对称的,
i的对称位置i1的回文区域L1<L了,在C回文区之外了,
咱们至少现在能保证L–R内是完全对称的,所以目前来看
至少pArr[i]=R-i+1的,也就是至少,i–R是回文的【因为i1的回文区L–R1内是回文的,对称过来R2–R也必然回文】
还能外R之外扩吗?不能!
因为当初C由于a!=y字符,所以导致C最远能扩到R,你现在也休想扩,否则就矛盾了!
故pArr[i]=R-i+1,从i最远只能扩到R处

其实,这里你能发现啥呢?咱们来到i,有了对称位置i1的pArr信息,咱们可以省掉i–R从头扩的步骤,这就是舍弃思想的妙处啊!

(3)i的对称位置i1的回文区域在C的回文区域上,即两者完全重合
即L1=L时
至少根据(2)咱们能知道目前从i最远能扩到R处,就目前至少故pArr[i]=R-i+1
那后续还能不能扩呢?暴力扩去吧,非常可能还可以扩!
咱们看例子
在这里插入图片描述
上图呢,L1=L了,重合,于是乎,最少咱们能把L2–R2定在上面绿色位置
由于R=1位置的字符是a,与L2-1处a相同,所以说,还能扩的,那咱们就暴力扩呗!
pArr[i]=R2-i+1

其实,这里你能发现啥呢?咱们来到i,有了对称位置i1的pArr信息,咱们可以最少能省掉i–R从头扩的步骤,这就是舍弃思想的妙处啊!

上面【1】【2】的(1)(2)(3)俩大情况,仨小情况
中,我们充分利用了舍弃思想
我们充分利用了舍弃思想
我们充分利用了舍弃思想
这就大大地加速了咱们填写pArr数组的速度,这就是manacher牛逼的地方!!!

手撕manacher算法的代码精致简介代码

有了上面收集pArr数组的过程,每次i拿到pArr[i],咱把最大值更新给max即可,最后,我们收了返回max-1就是咱要的结果

但是呢,写代码呀,咱们要有一个精简的过程
这么搞

情况【1】,当i>R时:你没得选,跟暴力扩展一样,直接从i往外扩
情况【2】,当i<=R时:分为3种小情况
我们在情况【2】这里,关注的是i位置,与C对称的那个位置i1,它的回文边界L1–R1是啥情况
如果C当前扩到的最左右边界是L–R的话
(1)i的对称位置i1的回文区域在C的回文区域内
即L<L1时,R1自然是<R的。这时,i1的回文半径就是i的回文半径,pArr[i]=pArr[i1]
(2)i的对称位置i1的回文区域在C的回文区域外
即L1<L时。这时,i的回文半径就是R-i+1,pArr[i]=R-i+1 ,也即是i的回文区域最远只能扩到R
(3)i的对称位置i1的回文区域在C的回文区域上,即两者完全重合
即L1=L时。至少根据(2)咱们能知道目前从i最远能扩到R处,就目前至少故pArr[i]=R-i+1
那后续还能不能扩呢?暴力扩去吧,非常可能还可以扩!

你瞅瞅?

咱们的精简代码是这样的流程,你明白了之前的思想,这个也就很好理解的。

1–是不是当i>R时,最次pArr[i]=1,就是i字符自己呗【情况1呗】
2–否则当i<=R时,咱们先扩1个基础步骤,取pArr[i]=min(2C-i,R-i),
说白了就是要舍弃pArr[i]这么多长度,不要重复对比了
2C-i代表啥呢?i的对称位置i1的回文半径,比较小,求法就是2C-i,你记住就行。
R-i代表啥呢?上面情况【2】的(2)(3)中i的对称位置i1的回文半径,就是R-i这么多,咱们不要1,
1–2–整合直接就是一句代码:pArr[i] = i > R ? 1 : Math.min(2 * C - i, R - i);
3–咱们在1–2–之后才试图暴力往外扩,能扩的话就让pArr[i]++
4–每次扩完,都需要更新R,C,max,每个i可能都是未来的新的更远的C

你看字糊涂没关系,咱写代码,你看代码就能理解
代码非常简介,你学完必须自己要能手撕
手撕的前提就是必须理解这个manacher算法的本质

//复习manacher算法
    public static int manacherReview(String s){
        if (s.equals("") || s.length() == 0) return 0;
        int max = Integer.MIN_VALUE;//先默认负无穷
        
        //s填写虚轴符号#
        char[] str = tomanacherString(s);
        int N = str.length;//新s的长度,也会是pArr的长度
        int[] pArr = new int[N];

        //初始化R,C
        int C = -1;
        int R = -1;//此前已经扩到的最右边界

        //i从0--N-1更新max
        for (int i = 0; i < N; i++) {
            //1--是不是当i>R时,最次pArr[i]=1,就是i字符自己呗【情况1呗】
            //2--否则当i<=R时,咱们**先扩1个基础步骤**,取pArr[i]=min(2C-i,R-i),
            //**说白了就是要舍弃pArr[i]这么多长度,不要重复对比了**
            //2C-i代表啥呢?i的对称位置i1的回文半径,比较小,求法就是2C-i,你记住就行。
            //R-i代表啥呢?上面情况【2】的(2)(3)中i的对称位置i1的回文半径,就是R-i这么多,咱们不要1,
            //以上几个基础步骤啊,直接统一成依据代码
            pArr[i] = i > R ? 1 : Math.min(2 * C - i, R - i);
            //3--咱们之后才**暴力往外扩,能扣就让pArr[i]++**
            while (i - pArr[i] >= 0 && i + pArr[i] < N){
                //i想外扩的话,这俩位置不能越界
                //如果他俩相等,半径++
                if (str[i - pArr[i]] == str[i + pArr[i]]) pArr[i]++;
                else break;//一旦不能扩了,直接退出
            }

            //4--每次扩完,都需要更新R,C,max,每个i可能都是未来的新的更远的C
            if (i + pArr[i] > R) {
                R = i + pArr[i];
                C = i;//新边界来了
            }
            max = Math.max(max, pArr[i]);//带有#的s的最长回文半径
        }

        return max - 1;//结果怎么推的看文章解释
    }

测试一把,问题不大:

    public static void test2(){
        int[] arr = {3,1,2,1,1,2,1,4};
        //System.out.println(getBackMaxLen(arr));
//        char[] chars = tomanacherString("121");
        System.out.println(manacher("babad"));
        System.out.println(manacherReview("babad"));
    }

    public static void main(String[] args) {
//        test();
        test2();
    }
3
3

是不是超级牛!
这个要是在面试中,你能手撕出来,说明你的算法优化能力还是会很强的,面试官自然能高看你!


总结

提示:重要经验:

1)回文串的基本概念,回文半径,回文区域,回文右边界,中心啥的,理解透彻
2)整个manacher算法的关键在如何舍弃不需要再对比的地方,然后填写回文半径数组pArr,最后更新给max
3)笔试求AC,可以不考虑空间复杂度,但是面试既要考虑时间复杂度最优,也要考虑空间复杂度最优。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冰露可乐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值