详解马拉车算法中的半径数组计算方法

最近有个朋友给我介绍tx的面经时提到了最长回文子串问题,这才知道原来还有时间复杂度为O(n)的解法——Manacher算法,于是去翻了翻博客,发现对于半径数组p的讲解都非常难懂,为了节约以后自己复习算法的时间,特地写一篇博客来好好理解这一部分的内容。在浏览本文前,请确保自己已了解O(n2)的算法,因为Manacher算法本质上是对于O(n2)算法的一种优化
先给出我学习这个算法时主要参考的博客:https://blog.csdn.net/happyrocking/article/details/82622881

一、两个辅助指针

在计算半径数组p时,一般会涉及到两个指针:id和mx,这两个指针是根据已发现的右边界位置最大的回文子串来定义的(请区分实际的最长回文子串和”已发现的右边界位置最大的回文子串“),其中id是上述回文子串的中心位置,mx是上述回文子串的右边界位置(也就是末尾的位置)。
举个例子:
假设字符串s="#g#o#o#g#l#e#"

位置i12345678910111213
s[i]#g#o#o#g#l#e#

和O(n^2)的算法类似,我们每次以位置i为中心寻找最长回文子串。
当i=4时,已发现的右边界位置最大的回文子串是“#o#”,我们可以更新id=4,mx=5。
当i=5时,已发现的右边界位置最大的回文子串是“#g#o#o#g#”,我们更新id=5,mx=9。

二、利用回文串特性降低时间复杂度

可能有人会有疑问:既然还是要像O(n2)的算法一样以位置i为中心寻找最长回文子串,那是怎么做到把时间复杂度降低为O(n)的呢?其关键就在于mx——当i<mx时,我们利用回文串的特性来避免“以位置i为中心寻找最长回文子串”这一操作。如果i<mx时每次操作的复杂度是O(1),那么一旦mx为原字符串最末端,那么之后的总操作复杂度最多也就是O(n);而mx扩展到最末端所需的时间复杂度最多也是O(n),所以整个算法的时间复杂度就是O(n)。
以上说法只是为了便于理解,非常不严谨,真正的推导过程请自寻查找。
那么接下来的重点就很明显了:当i<mx时,如何利用回文串的特性来避免“以位置i为中心寻找最长回文子串”这一操作呢?很多博客都是用下面这张图来讲的(包括我主要参考的那篇):
在这里插入图片描述
首先明确一下这张图里的内容:假设当前位置为 i(我们已经得到了 p[0…i-1],想要计算出p[i] )。红1是以 j 为中心的最长回文子串,红2是以 i 为中心的最长回文子串,红3是以 id 为中心的最长回文子串(首尾两端分别为“mx的对称点”和mx)。其中 i 和 j 以id为中心对称,即 i + j = 2*id。
这里有两种情况:
1、红2的末尾位置小于mx,和图中的情况一样
由于红1和红2都在红3中且以id为中心对称,故根据回文串特性,我们知道红1=红2;而已知红1是以 j 为中心的最长回文子串,所以红2也是以 i 为中心的最长回文子串(红2不可能更长,否则红1也要更长,这就矛盾了),故p[i]=p[j]。
2、红2的末尾位置大于mx,和图中的情况不一样
同样根据回文串特性,红2中从 i 到mx这一段一定是回文的,而之后的内容则需要遍历判断,所以我们可以先令p[i]=mx-i,然后通过类似于“以位置i为中心寻找最长回文子串”的操作(也就是一个while循环)来判断s[ i + p[i] + k ]与s[ i - p[i] - k ]的关系,从而得到真正的p[i]。同时我们还要更新id和mx
代码直接引用我所参考的博客中的python实现了,其中 id_c 即为 id,且特殊字符使用的是 \0,\1,\2

def longestPalindrome5(s):
    if len(s) <= 1:
        return s

    # 每个字符之间插入 \1	
	ss = '\0\1' + '\1'.join([x for x in s]) + '\1\2'
	p = [0] * len(ss)
    id_c = 0
    mx = 0
    max_str = ''
    for i in range(1, len(p)-1):
        if i < mx:
            j = 2 * id_c - i 
            p[i] = min(mx-i, p[j])

        # 以位置i为中心寻找最长回文子串
        # 如果是i<mx的第1种情况,肯定不会进入循环
        # 如果是i<mx的第2种情况,p[i]=mx-i,从mx之后继续寻找以位置i为中心寻找最长回文子串
        # 如果i>mx,那么p[i]一开始为0
        # 不必判断是否溢出,因为这里假设首位和末尾均有特殊字符,肯定会退出
        while s[i - p[i] - 1] == s[i + p[i] + 1]: 
            p[i] += 1

        # 更新id和mx
        if i + p[i] > mx:
            mx = i + p[i]
            id_c = i

        # 更新最长串
        if 1 + 2 * p[i] > len(max_str):
            max_str = s[i - p[i] : i + p[i] + 1]

    return max_str.replace('\1', '')
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值