Manacher 算法

关于上篇文章中心扩散算法的实例发现了更为简便的方法https://blog.youkuaiyun.com/y764903157/article/details/103256622
Manacher 算法
个人觉得Manacher 算法是基于“中心扩散法”,采用和 kmp 算法类似的思想来的。
Manacher 算法,被中国程序员戏称为“马拉车”算法。它专门用于解决“最长回文子串”问题,时间复杂度为 O(N)。
维基百科中对于 Manacher 算法是这样描述的:

[Manacher(1975)] 发现了一种线性时间算法,可以在列出给定字符串中从字符串头部开始的所有回文。并且,Apostolico,
Breslauer & Galil (1995)
发现,同样的算法也可以在任意位置查找全部最大回文子串,并且时间复杂度是线性的。因此,他们提供了一种时间复杂度为线性的最长回文子串解法。替代性的线性时间解决
Jeuring (1994), Gusfield (1997)提供的,基于后缀树(suffix trees)。也存在已知的高效并行算法
Manacher 算法本质上还是中心扩散法,只不过它使用了类似 KMP 算法的技巧,充分挖掘了已经进行回文判定的子串的特点,在遍历的过程中,记录了已经遍历过的子串的信息,也是典型的以空间换时间思想的体现。

下面介绍 Manacher 算法的具体流程。

第 1 步:对原始字符串进行预处理(添加分隔符)
首先在字符串的首尾、相邻的字符中插入分隔符,例如 “babad” 添加分隔符 “#” 以后得到 “#b#a#b#a#d#”。

对这一点有如下说明:

1、分隔符是一个字符,种类也只有一个,并且这个字符一定不能是原始字符串中出现过的字符;

2、加入了分隔符以后,使得“间隙”有了具体的位置,方便后续的讨论,并且新字符串中的任意一个回文子串在原始字符串中的一定能找到唯一的一个回文子串与之对应,因此对新字符串的回文子串的研究就能得到原始字符串的回文子串;

3、新字符串的回文子串的长度一定是奇数;

4、新字符串的回文子串一定以分隔符作为两边的边界,因此分隔符起到“哨兵”的作用。
在这里插入图片描述
第 2 步:计算辅助数组 p

辅助数组 p 记录了新字符串中以每个字符为中心的回文子串的信息。

手动的计算方法仍然是“中心扩散法”,此时记录以当前字符为中心,向左右两边同时扩散,记录能够扩散的最大步数。

以字符串 “abbabb” 为例,说明如何手动计算得到辅助数组 p ,我们要填的就是下面这张表。

char	#	a	#	b	#	b	#	a	#	b	#	b	#
index	0	1	2	3	4	5	6	7	8	9	10	11	12
p

第 1 行数组 char :原始字符串加上分隔符以后的每个字符。

第 2 行数组 index :这个数组是新字符串的索引数组,它的值是从 00 开始的索引编号。

我们首先填 p[0]。
以 char[0] = ‘#’ 为中心,同时向左边向右扩散,走 11 步就碰到边界了,因此能扩散的步数为 0,因此 p[0] = 0;

char	#	a	#	b	#	b	#	a	#	b	#	b	#
index	0	1	2	3	4	5	6	7	8	9	10	11	12
p	    0

下面填写 p[1] 。
以 char[1] = ‘a’ 为中心,同时向左边向右扩散,走 11 步,左右都是 “#”,构成回文子串,于是再继续同时向左边向右边扩散,左边就碰到边界了,最多能扩散的步数”为 11,因此 p[1] = 1;

char	#	a	#	b	#	b	#	a	#	b	#	b	#
index	0	1	2	3	4	5	6	7	8	9	10	11	12
p	    0	1	

下面填写 p[2] 。
以 char[2] = ‘#’ 为中心,同时向左边向右扩散,走 11 步,左边是 “a”,右边是 “b”,不匹配,最多能扩散的步数为 0,因此 p[2] = 0;

char	#	a	#	b	#	b	#	a	#	b	#	b	#
index	0	1	2	3	4	5	6	7	8	9	10	11	12
p	0	1	0	

下面填写 p[3]。
以 char[3] = ‘b’ 为中心,同时向左边向右扩散,走 11 步,左右两边都是 “#”,构成回文子串,继续同时向左边向右扩散,左边是 “a”,右边是 “b”,不匹配,最多能扩散的步数为 11 ,因此 p[3] = 1;

char	#	a	#	b	#	b	#	a	#	b	#	b	#
index	0	1	2	3	4	5	6	7	8	9	10	11	12
p	    0	1	0	1			

下面填写 p[4]。
以 char[4] = ‘#’ 为中心,同时向左边向右扩散,最多可以走 44 步,左边到达左边界,因此 p[4] = 4。

char	#	a	#	b	#	b	#	a	#	b	#	b	#
index	0	1	2	3	4	5	6	7	8	9	10	11	12
p	    0	1	0	1	4				

继续填完 p 数组剩下的部分。
分析到这里,后面的数字不难填出,最后写成如下表格:

char	#	a	#	b	#	b	#	a	#	b	#	b	#
index	0	1	2	3	4	5	6	7	8	9	10	11	12
p	    0	1	0	1	4	1	0	5	0	1	2	1	0

说明:有些资料将辅助数组 p 定义为回文半径数组,即 p[i] 记录了以新字符串第 i 个字符为中心的回文字符串的半径(包括第 i 个字符),与我们这里定义的辅助数组 p 有一个字符的偏差,本质上是一样的。

下面是辅助数组 p 的结论:辅助数组 p 的最大值是 55,对应了原字符串 “abbabb” 的 “最长回文子串” :“bbabb”。这个结论具有一般性,即:

辅助数组 p 的最大值就是“最长回文子串”的长度。

因此,我们可以在计算辅助数组 p 的过程中记录这个最大值,并且记录最长回文子串。

简单说明一下这是为什么:

1.如果新回文子串的中心是一个字符,那么原始回文子串的中心也是一个字符,在新回文子串中,向两边扩散的特点是:“先分隔符,后字符”,同样扩散的步数因为有分隔符 # 的作用,在新字符串中每扩散两步,虽然实际上只扫到一个有效字符,但是相当于在原始字符串中相当于计算了两个字符。因为最后一定以分隔符结尾,还要计算一个,正好这个就可以把原始回文子串的中心算进去;
在这里插入图片描述
2.如果新回文子串的中心是 #,那么原始回文子串的中心就是一个“空隙”。在新回文子串中,向两边扩散的特点是:“先字符,后分隔符”,扩散的步数因为有分隔符 # 的作用,在新字符串中每扩散两步,虽然实际上只扫到一个有效字符,但是相当于在原始字符串中相当于计算了两个字符。
因此,“辅助数组 p 的最大值就是“最长回文子串”的长度”这个结论是成立的,可以看下面的图理解上面说的 22 点。
在这里插入图片描述
代码实现python版

class Solution:
    # Manacher 算法
    def longestPalindrome(self, s: str) -> str:
        # 特判
        size = len(s)
        if size < 2:
            return s

        # 得到预处理字符串
        t = "#"
        for i in range(size):
            t += s[i]
            t += "#"
        # 新字符串的长度
        t_len = 2 * size + 1
        # 当前遍历的中心最大扩散步数,其值等于原始字符串的最长回文子串的长度
        max_len = 1
        # 原始字符串的最长回文子串的起始位置,与 max_len 必须同时更新
        start = 0

        for i in range(t_len):
            cur_len = self.__center_spread(t, i)
            if cur_len > max_len:
                max_len = cur_len
                start = (i - max_len) // 2
        return s[start: start + max_len]

    def __center_spread(self, s, center):
        """
        left = right 的时候,此时回文中心是一条线,回文串的长度是奇数
        right = left + 1 的时候,此时回文中心是任意一个字符,回文串的长度是偶数
        """
        size = len(s)
        i = center - 1
        j = center + 1
        step = 0
        while i >= 0 and j < size and s[i] == s[j]:
            i -= 1
            j += 1
            step += 1
        return step

复杂度分析:

时间复杂度: O ( N 2 ) O(N^2) O(N2)这里 NN 是原始字符串的长度。新字符串的长度是 2 \times N+ 12×N+1,不计系数与常数项,因此时间复杂度仍为 O ( N 2 ) O(N^2) O(N2)
空间复杂度: O ( N ) O(N) O(N)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值