Manacher(马拉车)算法 清晰详解 代码分析 -查找字符串中所有回文子串

算法问题引入

Manacher算法是我在做LeetCode647题时所看到的,该算法的目的是为了更快的查找一字符串中的所有回文子串。
在网上看了不少文章和视频,都觉得比较晦涩难懂,所以这里我决定自己写一篇博客来解释这个算法,不仅仅是作为一个讲解,也是加深自己的理解,为了之后更好的复习与回顾。
下面,我们便来一步步的解释这个算法。

首先,我们要明确,什么是回文字符串。
百度百科中这样说:“回文串”是一个正读和反读都一样的字符串,比如“level”或者“noon”等等就是回文串。对于这两个单词,我们可以看到回文串的特点就在于对称二字,在level中,l与l对应,e与e对应,v与自己对应(它是中心点),而noon则是n与n对应,o与o对应。
这两个例子几乎涵盖了所有的回文串的两种情况,即奇数与偶数长度的情况。

然后是关于回文子串。对于一个回文串level,很明显的其子串eve也是一个回文串,其子串v也是一个回文串。所以对于level来说,它的回文子串总共有3个(考虑自身),我们可以用半径这个概念来形容回文串,比如level,其中心点到左右任意边界是3个单位(考虑自身),所以半径就是3,而noon,半径是2。所以一个回文串,它的回文子串的个数等于它的半径。

我们的问题在于,给定一个字符串,然后我们要去寻找它其中所有回文子串的个数
那么一个很好的思路(中心扩散)就出来了:
我们去遍历这个字符串的每个字符,然后把每个字符当做中心点来,然后向两边扩散,每次比较两边的数相等不相等,如果相等,结果+1(意味着找到一个新的回文子串),直到不相等为止,继续遍历。
如此一来便可以找到所有的回文子串了,这里存在两个问题。
问题1在于奇偶。奇数长度的回文子串中心点是单独一个,我们只要围绕中心点,然后比较中心点两侧的字符是否相同就可以了,所以奇数是我们比较喜欢的,因为简单。而偶数长度就比较麻烦,比如noon,中心点是哪一个呢,不好说,当然,我们可以这样两个指针如果是偶数,则一个指向前一个o,一个指向后一个o,而如果是奇数,都指向level中的v。这样每次奇偶来讨论也可以,不过要麻烦一些,而且复杂度也要高一些。
问题2在于,整体的复杂度比较高,最坏情况下是O(n^2),这就不喜欢了。

所以这时manacher算法就来救场了,它便是解决这两个问题。
简单的说,所谓manacher算法,就是我们上面的比价暴力的中心扩散方法的一个改进,加了一些判断条件,使得其中很多步骤不用重复计算,从而一定程度上减少时间复杂度。(就像dp一样,记录下算过的值,防止重复计算)。

Manacher算法核心所在

为了解决上述两个问题。
manacher算法分别采取了如下的操作。

首先是关于回文串奇偶的问题。该算法采取了这样一个操作。它对字符串进行修改,eg:将 ’#‘ 插入字符串中(#只是一个辅助符号,它不会对原字符串造成信息上的干扰)。
在这里插入图片描述
这样一来,我们就不用考虑奇数偶数问题了。可以看到我们需要处理的字符串,无论一开始是奇是偶,现在统一都成了奇数了,这是我们想看到的情况。
然后我们再来看一下我们插入了多少个#。对于长为n的字符串,首先我们在中间插入n-1个,然后在两边插入2个,所以我们总共插入了n+1个,所以我们最终经过处理的字符串长度是2*n+1。

然后是关于第二个问题,我们如何去重复,来减小时间复杂度。
实际上,我们先来看这个重复情况,看完这个重复情况,你大概就能理解整个manacher算法的核心了。

之前说过,manacher算法是中心扩散思路的一种改进。我们现在遍历处理好的字符串,继续使用中心扩散来寻找以每个点为中心的回文串。
假设现在我们当前已经找到了一个回文串,它的右边界是最大的。
在这里插入图片描述
如上图所示,我们找到的回文串的中心命名为iMax,其右边界命名为rMax,左边界我标出来了,下面命名为了lMax。
那么我当前遍历的中心点是i,要在i做中心扩散来找回文串,但是想这样一个问题,我们真的有必要一个一个的去扩散比较吗
在这里插入图片描述

如上图,我们要紧扣住回文字符串的对称特性。上图中s1与s2是两个关于iMax对称的且包含于[lMax,rMax]区间之内的(lMax是我临时取得,懂得都懂)。所以他们两必定是对称相等的。若s1是回文串,则s2也一定是回文串。

那么很明显,我目前的i肯定是大于iMax的(因为iMax是之前遍历的加上中心扩散找到的嘛),也很明显,s1之前我也已经确定过是回文串(假设),那么此时我就没有必要去再判断一遍s2是不是回文串了,因为对称,所以它一定是!

如此一来,相比你差不多已经明白了manacher算法为何能够降低时间复杂度了。原因在于我是从左到右遍历,左边已经判断过的,右边又何必再判断一次呢?直接因为对称所以一样就完事了。(是不是很像求对称函数的单调性?)

当然这个东西没那么简单,要去实现它,我们需要辅助的数据结构以及多种情况判断。

Manacher算法的逻辑分析

首先我如何去知道s1那段是什么情况呢?也就是说s1这个回文串多长?中心点是谁?半径多大?
这里算法里用了一个数组f,f的长度是2*n+1(和处理后的字符串一样,相对应)。
f[i]存储的是以当前点为中心点而能形成的最长回文串的半径。

那么假设我们遍历到m这个点的时候,我们只需要看其关于iMax对称的n点的f[n],就能知道f[m]是大于等于f[n]的了(以m为中心点的回文串的半径至少是f[n]了,然后再中心扩散,直接从m+f[n]+1开始比较,从而省去了f[n]-m次比较)。

当然,情况没这么简单,这里有多种情况要分类讨论。

情况1:我们的i是小于rMax的
那么因为对称,我们便能通过与i关于iMax对称的2*iMax-i点来获取f[i]。
但是这存在两种情况。
情况1之小情况A
在这里插入图片描述

当我们i的对称点所找到的最大回文串也在我们的[lMax,rMax]内,那么理所应当,f[i] = f[2*rMax-i]。
这没问题。
但是,情况1之小情况B
在这里插入图片描述
明显,这里s1的范围超出了[lMax,rMax],超出的部分就不满足对称特性了,所以我们这里给f[i]的赋值只能是我们能够确定的最大值,即rMax-i+1。

情况2
这个就很好理解了,i>rMax。
在这里插入图片描述
显然此时我们没法在[lMax,rMax]区间内找一个与i关于iMax对称的点,没法以此来给f[i]赋值,但是我们知道,一个字母也是回文串,那么它的半径是1(考虑自身),所以此时我们令f[i]=1。

以上便是我们需要考虑所有的重复情况以及应对方式。

在确定了f[i]之后,并不意味着我们的以i为中心的最长回文串就找到了,我们还需要以f[i]为基础,继续中心扩散,来寻找最长。
同时,当我们新的边界超过了rMax时,我们还要更新iMax与rMax。

到此,manacher算法的逻辑就讲完了,下面我们来看代码,以下代码是java的。

Manacher代码实现

class Solution {
    public int countSubstrings(String s) {
        int n =s.length();
        StringBuffer t = new StringBuffer("$#");//这里左右多给了字符$!是为了保证数组从1开始,与rMax初始值避开
        //构建新的字符串,用#来填充
        for(int i =0; i<n; i++){
            t.append(s.charAt(i));
            t.append('#');
        }
        n = t.length();
        t.append('!');

        //f[i]
        int[] f = new int[n];
        int iMax= 0, rMax = 0, ans = 0; //iMax为边界最大的那个的中心,rMax为最大的右边界
        for(int i=1;i<n;i++){
            //初始化f[i] 
            f[i] = i<rMax? Math.min(rMax-i+1,f[2*iMax-i]) : 1;//情况1(min是小情况A与B的判断) 与情况2的判断
            //中心扩展  暴力扩展
            while(t.charAt(i+f[i])==t.charAt(i-f[i])){
                f[i]++;
            }
            //对iMax和rMax进行更新
            if(i+f[i]-1>rMax){
                iMax = i;
                rMax=i+f[i]-1;
            }
            //统计答案,当前贡献为f[i]-1/2向上取整  有多少个子串,同时因为多加入了#,要排除这个干扰
            ans += f[i]/2;
        }
        return ans;

    }
}
  • 6
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值