Manacher 算法是时间、空间复杂度都为 O(n) 的解决 Longest palindromic substring(最长回文子串)的算法。回文串是中心对称的串,比如 'abcba'、'abccba'。那么最长回文子串顾名思义,就是求一个序列中的子串中,最长的回文串。本文最后用 Python 实现算法,为了方便理解,文中出现的数学式也采用 py 的记法。

在 leetcode 上用时间复杂度 O(n**2)、空间复杂度 O(1) 的算法做完这道题之后,搜了一下发现有 O(n) 的算法。可惜英文 wikipedia 上的描述太抽象,中文介绍又没找到说的很明白的,于是就下决心自己写一篇中文比较清楚的。我弄明白这个算法是通过 leetcode 上的一篇文章,也就是 wikipedia 词条中第一个外部链接。链接在此(http://articles.leetcode.com/2011/11/longest-palindromic-substring-part-ii.html),图文并茂,很容易懂(我就是没通读文章,主要看图和图的说明就弄懂了)。如果还是觉得读英文费劲的话,那接着读我这篇吧。

Manacher 算法的最终目的,是根据原串构造出一个新队列,内容是以该点为中心,最长的对称长度。为了解决对称奇偶性的问题(比如 aba 和 abba,常规算法需要分成两种情况),首先是构造一个辅助串,在首尾和任何两字符中间插入一个相同的字符。比如串 ababa,构造成 #a#b#a#b#a#。接下来就是构造一个新队列,里面记录以该点为中心的最长对称长度:

S1 = ababa
T1 = # a # b # a # b # a #
P1 = 0 1 0 3 0 0 3 0 1 0 S2 = abaaba
T2 = # a # b # a # a # b # a #
P2 = 0 1 0 3 0 1 1 0 3 0 1 0

那么,具体该如何构造序列 P 呢?首先,去除串长不大于1的 corner case,我们总能得到 P 前两个元素的值。

T = # ? # ...
P = 0 1 ? ...

然后,我们就可以根据已经知道的 P 元素和 T 中的元素一步一步求出后面的值了。问题分解成:已知 P[:i],求 P[i] 的问题。

接下来,就要讨论一下回文子使 P 具有哪些性质。下面用 leetcode 文章中的例子,s = 'babcbabcbaccba'(len(s) == 14,t = '#b#a#b#c#b#a#b#c#b#a#c#c#b#a#',len(t) == len(s)*2+1 == 29)。


1.核心算法

如下图,假设当我们已知 T、P[:8] 时,求 P[9]。

     0 1 2 3 4 5 6 7 8 9 0 1 2 3 4  5 6 7 8 9 0 1 2 3 4 5 6 7 8
T = [# b # a # b # c # b # a # b #] c # b # a # c # c # b # a #
P = [0 1 0 3 0 1 0 7 0 ? ...

观察我用方括号包起来的部分,正是以 T[7] 为中心,7为长度构成的回文。直观来看,由于回文是对称的结构,P 中的元素值似乎也应该是根据中心对称的,那么 P[9] = P[5] = 1,从结果上来看也是正确的。那么接下来往后填,很快你会发现这个结论有错误。

     0 1  2 3 4 5 6  7 8 9 0 1 2 3 4  5 6 7 8 9 0 1 2 3 4 5 6 7 8
T = {# b [# a # b #} c # b # a # b #] c # b # a # c # c # b # a #
P = 0 1 0 3 0 1 0 7 0 1 0 ? ...
i = 11
center = 7
right = 14
mirror = 3

当填到 P[11] 时,红色字部分是 T[7] 为中心7为长度的回文,而黄色背景色部分,是以 P[11] 为中心9为长度的回文。按照上一段的结论,我们应该填入 P[7-(11-7)] = P[3] = 3,但实际上应该填入9。这是怎么回事呢?

为了说清楚这个问题,先来定义一些变量。首先 center 是已知的对称点,right 是已知对称点的最右端,当前求的 P 索引为 i,i 关于 center 的对称点索引是 mirror。上面在求 P[9] 和 P[11] 时,center == 7,right == 14。接下来,让我们接着往后扫描,看一下另外一个情况:已知 center==11, right==20, 求 P[15]。

     0 1  2 3 4 5 6 7 8 9 0 1 2 3 4  5 6 7 8 9 0  1 2 3 4 5 6 7 8
T = {# b [# a # b # c # b # a # b #} c # b # a #] c # c # b # a #
P = 0 1 0 3 0 1 0 7 0 1 0 9 0 1 0 ? ...
i = 15
center = 11
right = 20
mirror = 7

观察这两个过程,不难发现,发生这种情况的原因,是 mirror 为中心点的回文(示例中用黄色背景标注,{}之间的回文),其范围超过了以 center 为中心点的回文的左端(红字标注,[] 之间的回文)。而凡是 P[i] == P[mirror] 的回文,其 P[mirror] 的范围都不超过 P[center] 的范围。具体来说,就是 P[mirror] 的左端不超过 P[center] 的左端。

那么怎么来判断呢?mirror 到 P[center] 左侧的长度是 right-i,如果这段长度不小于 P[mirror] 的话,P[mirror] 就在 P[center] 的范围内。如果没想明白的话,下面是用笨方法来的数学推导:

PalindromeLeftOf(mirror) = mirror - P[mirror]
PalindromeLeftOf(center) = center - (right - center)
mirror = center - (i - center) if leftOf(mirror) is inside P
PalindromeLeftOf(center) <= PalindromeLeftOf(mirror) =>
center - (right - center) <= mirror - P[mirror] =>
center - (right - center) <= center - (i- center) - P[mirror] =>
-right <= -i - P[mirror] =>
P[mirror] <= right - i

需要注意的是,由于 P[mirror] == right - i  情况的存在,也就是 P[mirror] 的左端与 P[center] 的左端重合,这也就意味着 P[i] 的当前右端为 right,P[i] 也就有可能会比 P[mirror] 长,这种情况下仍需对 P[i] 进行扩展操作。

到目前为止,通过已知的 p[:i] 用 O(1) 的操作计算得出 p[i] 的结果的过程就讲完了。这个过程是 Manacher 算法的核心,也是最难理解的部分,剩下的过程就不复杂了。


2.扩展 P 的过程

现在再回到本文一开始,当我们拿到源数据时,马上可以得到的是 P 的前两项。

T = # b # a # b # c # b # a # b # c # b # a # c # c # b # a #
P = 0 1 ? ...
center = 1
right = 2

开始计算,i=2, mirror=0, P[mirror] == right-i。按照上一节的结论,需要对 P 进行扩展,也就是对 T[i+n] == T[i-n] 进行依次判断。不幸的是,第一次就失败了,于是我们进入下一步:

T = # b # a # b # c # b # a # b # c # b # a # c # c # b # a #
P = 0 1 0 ? ...
i = 3
center = ?
right = 2
mirror =?

不论是以 P[1] 为中心,还是 P[2] 为中心,right 的值总是2,而下一步 i > right,这时不论哪个是中心,都不存在 i 的对称点了。那这种情况下,我们就从零开始吧:

    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8
T = # b # a # b # c # b # a # b # c # b # a # c # c # b # a #
P = 0 1 0 3 ? ...
i = 4
center = 3
right = 6
mirror = 2

这次我们得到了新的 center、right,并且 right 比原来的大,那么就用新的来替换老的。

到现在为止,i > right 和 P[mirror] <= right - i 两种情况都已经讨论完了,那么还剩最后一种。


3.最后一步

现在还剩下的情况就剩 i <= right and P[mirror] > right - i 这一种了。既然是要扩展 P,那么从零开始似乎也没什么不妥的。先考虑下面的情况:

    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8
T = # b # a # b # c # b # a # b # c # b # a # c # c # b # a #
P = 0 1 0 3 0 1 0 0 1 0 0 1 0 ? ...
i = 15
center = 11
right = 20
mirror = 7

这种情况前面已经见过一次了 P[mirror] > right-i。这时,我们需要扩展 P[15],但是需要从 n=0 开始判断 P[15+n] == P[15-n] 成立吗?因为 P[mirror]=7 > right-i=5,由于当 0<=n<=7 时,有 P[mirror-n] == P[mirror+n],而当 0<=n<=5 时有 P[mirror-n] == P[i+n],所以不难得出当 0<=n<=5 时有 P[i+n] == P[i-n]。换句话说,由于 P[mirror] 的回文范围超过了 P[center] 的范围,所以 P[i] 在不超过 P[center] 的范围内一定是回文。所以这时我们不需要从0开始判断,而只要从 right+1 开始判断就可以了。


代码

再上代码之前,首先对上述情况进行重分类和合并。

1. 直接用 O(1) 操作得到 P[i],不需要对 P 进行扩展,也不需要扫描 T,center、right 的值也不变。

2. 需要对 P 进行扩展,这时会从 T[right+1] 开始扫描,同时也需要更新 center、right 的值。

情况1的复杂度为 O(1),情况2的复杂度虽然为 O(n),但由于算法的实现,能够保证从左到右扫描 T 时,每次都从 right+1 开始,并且扫描的最右端成为 right,这样就能够保证从左到右访问 T 中的每个元素不超过2次。所以,Manacher 算法的复杂度是 O(n)。

那么按照我的分类,贴上一段 Python 代码的实现,可能会跟你见到的标准实现长得不太一样。

 # @param {string} s
# @return {string}
def longestPalindrome(s):
if len(s) <= 1:
return s THE_ANSWER = 42
T = [THE_ANSWER]
for c in s:
T.append(c)
T.append(THE_ANSWER) c, r, size = 1, 2, len(T)
P = [0, 1] + [None] * (size-2)
maxIndex, maxCount = 0, 1
for i in xrange(2, size):
m = c*2 - i # mirror = center - (i - center)
if r > i and P[m] < r-i:
# case 1, just set P[i] <- P[m]
P[i] = P[m]
continue # case 2, expand P
count = min(i, size-i-1) # n's limit
# scan, from if r <= i then T[i+1] else T[right+1]
for n in xrange((1 if r <= i else r+1-i), count+1):
if T[i+n] != T[i-n]:
count = n-1
break # update center and right, save P[i], compare with the max
c = i
r = i+count
P[i] = count
if count > maxCount:
maxCount = count
maxIndex = i-count maxIndex = maxIndex // 2
return s[maxIndex:maxIndex+maxCount]

Manacher's algorithm: 最长回文子串算法的更多相关文章

  1. Manacher 求最长回文子串算法

    Manacher算法,是由一个叫Manacher的人在1975年发明的,可以在$O(n)$的时间复杂度里求出一个字符串中的最长回文子串. 例如这两个回文串“level”.“noon”,Manacher ...

  2. manacher求最长回文子串算法

    原文:http://www.felix021.com/blog/read.php?2040 首先用一个非常巧妙的方式,将所有可能的奇数/偶数长度的回文子串都转换成了奇数长度:在每个字符的两边都插入一个 ...

  3. Algorithm --> 最长回文子串

    1.中心扩展 中心扩展就是把给定的字符串的每一个字母当做中心,向两边扩展,这样来找最长的子回文串.算法复杂度为O(N^2). 但是要考虑两种情况: 1.像aba,这样长度为奇数. 2.想abba,这样 ...

  4. manacher求最长回文子串算法模板

    #include <iostream> #include <cstring> #include <cstdlib> #include <stdio.h> ...

  5. LeetCode:Longest Palindromic Substring 最长回文子串

    题目链接 Given a string S, find the longest palindromic substring in S. You may assume that the maximum ...

  6. 转载:LeetCode:5Longest Palindromic Substring 最长回文子串

    本文转自:http://www.cnblogs.com/TenosDoIt/p/3675788.html 题目链接 Given a string S, find the longest palindr ...

  7. 最长回文子序列/最长回文子串(DP,马拉车)

    字符子串和字符子序列的区别 字符字串指的是字符串中连续的n个字符:如palindrome中,pa,alind,drome等都属于它的字串 而字符子序列指的是字符串中不一定连续但先后顺序一致的n个字符: ...

  8. 【转】最长回文子串的O(n)的Manacher算法

    Manacher算法 首先:大家都知道什么叫回文串吧,这个算法要解决的就是一个字符串中最长的回文子串有多长.这个算法可以在O(n)的时间复杂度内既线性时间复杂度的情况下,求出以每个字符为中心的最长回文 ...

  9. 最长回文子串(Manacher算法)

    回文字符串,想必大家不会不熟悉吧? 回文串会求的吧?暴力一遍O(n^2)很简单,但当字符长度很长时便会TLE,简单,hash+二分搞定,其复杂度约为O(nlogn), 而Manacher算法能够在线性 ...

随机推荐

  1. 关于HTML面试题汇总之H5

    一.H5有哪些新特性,移除了哪些元素?如何处理h5新标签的浏览器兼容性问题,如何区分html和html5 1. html5不在是SGL(通用标记语言)的一个子集,而包含了:图像.位置.存储.多任务等功 ...

  2. Photopile JS – 帮助你实现精致的照片堆叠效果

    Photopile JS 是模拟照片散布堆叠在一起的 JavaScript/jQuery 图片库.点击缩略图,照片会弹出放大 ,再次点击照片会返回.缩略图是可拖动的,允许照片深深的堆在一起而不被覆盖, ...

  3. Riot - 比 Facebook React 更轻量的 UI 库

    Riot 是一个类似 Facebook React 的用户界面库,只有3.5KB,非常轻量.支持IE8+浏览器的自定义标签,虚拟 DOM,语法简洁.Riot 给前端开发人员提供了除 React 和 P ...

  4. 实例之JavaScript

    使用JavaScript实现5秒倒计时 <html> <head> <meta charset="utf-8"> <title>&l ...

  5. Atitit.复合文档的格式 标准化格式

    Atitit.复合文档的格式 标准化格式 1. Docfile1 2. Iso   Cdf  cd file1 3. Zip1 4. Ooxml1 5. Odf  :OpenDocument Form ...

  6. Source Insight基本使用和快捷键

    Source Insight基本使用和快捷键 为什么要用Source Insight呢?貌似是因为比完整的IDE要更快一些,比较利于查看大量的代码. 软件的安装很简单,设置好安装目录. 配置好文档路径 ...

  7. Android 4.4 KitKat 新特性

    New in Android 4.4 KitKat 本文是一个概览,关于KitKat,也即Android4.4的新东西,先是功能型的,之后是设计上的. 很多特性本文并没有提到,很多提到的特性也只是简短 ...

  8. leetcode 第188题,我的解法,Best Time to Buy and Sell Stock IV

    <span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255) ...

  9. 【iOS】小项目框架设计(ReactiveCocoa+MVVM+AFNetworking+FMDB)

    上一个项目使用到了ReactiveCocoa+MVVM+AFNetworking+FMDB框架设计,从最初的尝试,到后来不断思考和学习,现在对这样一个整体设计还是有了一定了理解与心得.在此与大家分享下 ...

  10. iOS 9 使用HTTP的方法

    问题 在ios 9中使用HTTP请求,Xcode就会抛出下面的Exception: App Transport Security has blocked a cleartext HTTP (http: ...