写在前面

manacher算法解决最长回文子串以及变形问题的时间复杂度为O(n)。

如果你想囫囵吞枣,只需要使用到该算法,你可以直接把代码拿走;但如果你想深入了解这个算法的工作原理和关键部分解读,还是希望你能静下心来,拿出一张纸,一根笔,来好好揣摩算法背后的魅力,算法解读部分内容来自《来自于程序员代码面试指南:IT名企算法与数据结构题目最优解》,理解起来的确有点冗杂和费力。(以下来自网友真实案例)

Python代码(Leetcode 5为例)

class Solution:def manacherString(self,s):charArr = list(s)res = []index = 0for i in range(0, len(charArr) * 2 + 1):if (i & 1) == 0:res.append("#")else:res.append(charArr[index])index += 1return resdef longestPalindrome(self, s):""":type s: str:rtype: str"""# if s is None or len(s) == 0:#     return NonecharArr = self.manacherString(s)pArr = []index = -1pR = -1max_value =  -11111# maxContainsEnd = -1for i in range(0,len(charArr)):if pR > i:pArr.append(min(pArr[2*index -i],pR-i))else:pArr.append(1)while i + pArr[i] < len(charArr) and i - pArr[i] > -1:if charArr[i + pArr[i]] == charArr[i - pArr[i]]:pArr[i] += 1else:breakif i + pArr[i] > pR:pR = i + pArr[i]index = imax_value = max(max_value, pArr[i])result1 = pArr.index(max_value)result2 = charArr[result1-max_value+1:result1+ max_value]for i in range(0,len(result2),2):result2.remove('#')result2 = ''.join(result2)return result2
#leetcode Test 最快的答案
class Solution:def longestPalindrome(self, s):""":type s: str:rtype: str"""if len(s) < 2 or s == s[::-1]:return smax_len = 1start = 0for i in range(1,len(s)):even = s[i - max_len : i + 1]odd = s[i - max_len - 1 : i + 1]if i - max_len - 1 >= 0 and odd == odd[::-1]:start = i - max_len - 1max_len += 2continueif i - max_len >= 0 and even == even[::-1]:start = i - max_lenmax_len +=  1return s[start : start + max_len]

Manacher算法详解

该算法是由Glenn Manacher于1975年首次发明的,Manacher算法解决的我呢提是在线性时间内找到一个字符串的最长回文子串,比起能够解决该问题的其他算法,Manacher算法算比较好理解和实现的。

数据处理与“扩”

以一般的思维,从左到右遍历字符串,遍历到每个字符的时候,都要看以这个字符作为中心能够产生多大的回文字符串。如str="abacaba",最长回文子串是以str[3]='c'为中心时,最大长度的7,以str[0]='a'为中心,回文子串长度为1,以str[1]='b'为中心,回文子串长度为3。这种是有中心的,但是如str=“abba”,最长回文子串长度为4,但中心并不在某个元素上,而是一个“虚轴”,在两个'b'之间。这是就出现第一个问题,当最长回文长度是偶数时,我们无法找到这个回文,因为它没有确切的中心。同时这种方法存在第二个问题,之前遍历过的字符完全无法指导后面遍历的过程,也就是对每个字符来说都是从自己的位置出发,往左右两个方向扩出去检查。这样,对每个字符来说,往外扩对的代价都是一个级别的。举一个极端的例子“aaaaaaaaaaaaaa”,对每一个'a'来讲,都是扩到边界才停止。所以每一个字符扩出去检查的代价都是O(n),所以总的时间复杂度O(n²)。Manacher算法可以做到O(n)的时间复杂度,精髓是之前字符的“扩”过程,可以指导后面字符的“扩”过程,使得每次的“扩”过程不都是从无开始。以下是Manacher算法解决原问题的过程:

1.数据处理

因为奇回文和偶回文在判断时比较麻烦,所以对str进行处理,把每个字符开头、结尾和中间插入一个特殊字符’#‘来得到一个新的字符串数组。比如str=”bcbaa“,处理后为"#b#c#b#a#a#",对于元素为偶数个的字符串同样处理,也可以得到奇数个的字符串,(原理:以下图为例,奇+偶=奇),经过数据处理,得到的字符串都是奇数位的,解决了字符串长度差异问题,同时‘#’的加入并不影响原始数据的回文序列

然后从每个字符左右扩出去的方式找最大回文子串就方便多了。对于奇回文来说,不这么处理也能通过扩的方式找到,比如"bcb",从'c'开始向左右两侧扩出去能找到最大回文。处理后为"#b#c#b#",从'c'开始向左右两侧扩出去依然能找到最大回文。对偶回文来说,不处理而直接通过扩的形式是找不到的,比如"aa",因为没有确定的中心,但是处理后'#a#a#',就可以通过从中间的‘#’扩出去的方式找到最大回文。所以通过这样的处理方式,最大回文子串无论是偶回文还是奇回文,都可以通过统一的“扩”过程找到,解决了差异性的问题。同时要说的是,这个特殊字符是什么无所谓,甚至可以是字符串中出现的字符,不会影响最终的结果,就是一个纯辅助的作用。

2.辅助变量

假设str处理之的字符串记为charArr。对每个字符(包括特殊字符)都进行“优化后”的扩过程。解释以下三个辅助变量的意义:

数组p。长度与charArr长度一样。p[i]的意义是以i位置上的字符(charArr[i])作为回文中心的情况下,扩出去得到的最大回文半径是多少,设只有自身时的回文半径为1,举个例子说明,对"#c#a#b#a#c#"来说,p[0...10]为[1,2,1,2,1,6,1,2,1,2,1]。整个过程就是在从左到右遍历的过程中,依次计算每个位置的最大回文半径值。

i 0 1 2 3 4 5 6 7 8 9 10
charArr[i] # c # a # b # a # c #
p[i] 1 2 1 2 1 6 1 2 1 2 1

整数pR。这个变量的意义是之前遍历的所有字符的所有回文半径中,最右即将到达的位置(将到未到的位置,以上面charArr[1]='c'为例,pR为3)。还是以"#c#a#b#a#c#"为例,还没有遍历之前,pR初始设置为-1,charArr[0]='#'的回文半径为1,所以目前回文半径向右只能扩到位置0,回文半径最右即将到达的位置变成了1(pR=1)。charArr[1]='#'的回文半径为2,此时所有的回文半径向右能扩到位置2,所以回文半径最右即将到达的位置变为3(pR=3)。charArr[2]== '#'的回文半径为1,所以位置2向右只能扩到位置2,回文半径最右即将到达的位置不变,仍是3(pR=3)。chaArr[3]='a'的回文半径为2,所以位置3向右能扩到位置4,所以回文半径最右即将到达的位置变为5(pR=5)。charArr[4]='#'的回文半径为1,所以位置4向右只能扩到位置4,回文半径最右即将到达的位置不变仍是5(pR=5)。 charArr[5]= 'b'的回文半径为6,所以位置4向右能扩到位置10,回文半径最右即将到达的位置变为11(pR=11)。 此时已经到达整个字符数组的结尾,所以之后的过程中pR将不再变化。换句话说,pR 就是遍历过的所有字符中向右扩出来的最大右边界。只要右边界更往右,pR就更新。

整数index。这个变量表示最近一次pR更新时,那个回文中心的位置。以刚刚的例子来说,遍历到charArr[0]时pR更新,index就更新为0.遍历到charArr[1]时pR更新,index就更新为1,......,遍历到charArr[5]时pR更新,index就更新为5。之后的过程中,pR将不再更新,所以index将一直是5。

3.“扩”

只要能够从左到右依次算出数组p每个位置的值,最大的那个值实际上就是处理后的charArr中最大的回文半径,根据更大的回文半径,再对应回原字符串的话,整个问题就解决了。第三步就是从左到右依次计算出p数组每个位置的值的过程。

1)假设现在计算到位置i的字符charArr[i],在i之前位置的计算过程中,都会不断地更新pR和index的值,即位置i之前的index这个回文中心扩出了一个目前最右的回文边界pR;

2)如果pR-1位置没有包住当前i位置。比如"#c#a#b#a#c#",计算到charArr[0]='#'时,pR为1。也就是说,右边界在1位置,1位置为最右回文半径即将到达但还没有到达的位置,所以当前的pR-1位置没有包住当前i位置。此时和普通做法一样,从i位置字符开始,向左右两侧扩出去检查,此时的“扩”过程没有获得“加速”("加速"的含义第3)点中会详细讲解);

3)如果pR-1位置包住了当前的i位置。比如“#c#a#b#a#c#”,计算到charArr[6...10]时,pR都是11,此时pR-1包住了位置6~10.这种情况下,检查过程是可以获得优化的(即加速),这也是manacher算法的核心内容。

在上图中,位置i是要计算回文半径(p[i])的位置 。pR-1位置此时是包住位置i的。同时根据index的定义,index是pR更新时那个回文中心的位置,所以如果pR-1位置以index为中心对称,即pR',那么从pR'位置到pR-1位置一定是以index为中心的回文串,既然回文半径数组p是从左到右计算的,所以位置i之前的所有位置都已经算过回文半径。假设位置i以index为中心向左对称过去的位置为i',那么i'的回文半径也是计算过的。那么以i'为中心的最大回文串大小(p[i'])必然只有三种情况,我们依次分析以下。

情况一:i'R'和i'R完全在pR'和pR内部(i'R'和i'R分别是以i'为中心时最大回文串的左边界和右边界)。即以i'为中心的最大回文串完全在以index为中心的最大回文串的内部,如下图所示。

令a'是i'R'位置的前一个字符,b'是i'R位置的后一个字符,b是b'以index为中心的对称字符,a是a'以index为中心的对称字符。iR是i'R'以index为中心的对称位置,iR'是i'R以index为中心的对称位置。如果处在情况一下,那么以位置i为中心的最大回文串可以直接确定,那么就是iR'到iR这一段。原因是,首先,i'R'到i'R这一段如果以index为回文中心,对应过去就是iR'到iR这一段,那么iR'到iR这一段完全是i'R'到i'R这一段的逆序。同时有i'R'到i'R这一段是回文串(以i'为回文中心),所以iR'到iR这一段一定也是回文串,也就是说,以位置i为中心的最大回文串起码是iR'到iR这一段。另外,以位置i'为中心的最大回文串只是i'R'到i'R这一段,说明a'!=b',那么必然有a!=b,说明以位置i为中心的最大回文串就是iR’到iR这一段,而不会扩的更大。(p[i']表示以i'为中心的最长回文半径,利用其可以加快后面的查找,这就是“加速”)

情况二:i'R在pR'和pR的内部,而i'R'在pR'的左侧,即外部,如下图所示。

令a'是pR'位置的前一个字符,b'是i'pR'位置的后一个字符,b是b'以index为中心的对称字符,c是a'以index为中心的对称字符。iR是i'R'以index为中心的对称位置,iR'是i'R以index为中心的对称位置,i'pR'是pR'以i'为中心的对称位置,ipR是pR以i为中心的对称位置。如果处在情况二下,那么以位置i为中心的最大回文串可以直接确定,就是ipR到pR这一段。原因是,首先,pR'到i'pR'这一段和ipR到pR这一段是关于index对称的,所以ipR到pR这一段是pR'到i'pR'这一段的逆序。同时,i'R'到i'R这一段是回文串(以i'为中心),那么pR'到i'pR'这一段也是回文串,所以pR'到i'pR'这一段的逆序也是回文串,所以ipR到pR这一段一定是回文串。也就是说,以位置i为中心的最大回文串起码是ipR到pR这一段。另外,i'R'到i'R这一段是回文串,说明a'=b',b'和b关于index对称,说明b==c,pR'到pR这一段没有扩更大,说明a'!=c,所以b!=c。说明以位置i为中心的最大回文串就是ipR到pR这一段,而不会扩的更大。

情况三:i'R'和pR'在同一位置,即以i'为中心的最大回文串压在了以index为中心的最大回文串的边界上,如下图所示。

i'R'和pR'的位置重叠,iR'是i'R位置以index为中心的对称位置,ipR是pR

位置以i为中心的对称位置,可以很容易的证明iR'和ipR位置也重叠。如果处在情况三下,那么以位置i为中心的最大回文串起码是ipR到pR这一段,但可能会扩的更大。因为ipR到pR这一段是i'R'和i'R这一段以index为中心对称过去的,所以两段互为逆序关系,同时i'R'到i'R这一段又是回文串,所以ipR到pR这一段一i的那个是回文串,但以位置i为中心的最大回文串是可能扩的更大的,pR的下一个元素是未知的,有可能ipR的前一个元素一样的,此时会发生扩更大的情况。以位置i为中心的最大回文串起码是ipR到pR这一段,但可以扩更大。说明在情况三下,扩出去的过程可以得到优化,但还是无法避免扩出去的检查。

结果处理与复杂度证明

1.结果处理

按照上面的操作从左到右计算出p数组,计算完成后再遍历一遍p数组,找出最大的回文半径,假设位置i的回文半径最大,即p[i]==max。但max只是charArr数组的最大回文半径,还得对应回原来的字符串,求出最大回文半径的长度(其实就是max-1)。比如原字符串为“121”,处理成charArr之后为“#1#2#1#”。在charArr中位置3的回文半径最大,最大值为4(即p[3]=4),对应原字符串的最大回文子串长度为4-1=3。

2.算法时间复杂度为O(n)的证明

虽然我们可以很明显地看到Manacher算法与普通方法相比,在扩出去检查这一行为上有明显的优化,但如何证明该算法地时间复杂度就是O(n)呢?关键之处在于估算扩出去检查这一行为发生的数量。原字符串在处理后地长度由n变成了2n,从算法地主要逻辑来看,要么在计算一个位置地回文半径时完全不需要扩出去检查,比如,算法步骤中介绍的三种情况,都可以直接获得位置i的回文半径长度;要么每一扩出去检查都会导致pR变量的更新,比如上面步骤中的情况2和情况3,扩出去检查时都让回文半径到达更右的位置,当然会使pR更新。然而pR最多是从-1增加到2n(右边界),并且从来不减小,所以扩出去检查的次数就是O(n)的级别。所以Manacher算法时间复杂度是O(n)。

进阶问题

在字符串的最后添加最少字符,使得整个字符串都成为回文串,其实就是查找在必须包含最后一个字符的情况下,最长的回文子串是什么。那么之前不是最长回文子串的部分逆序过来,就是应该添加的部分。比如“abcd123321”,在必须包含最后一个字符的情况下,最长的回文子串是“123321”,之前不是最长回文子串的部分是“abcd”,所以末尾应该添加的部分就是“dcba”,那么只要把manacher算法稍作修改就可以。具体改成:从左到右计算回文半径时,关注回文半径最右即将到达的位置(pR),一旦发现已经到达最后(pR==charArr.length),说明必须包含最后一个字符的最长回文半径已经找到,直接退出检查过程,返回该添加的字符串即可。

【最长回文子串】Manacher算法详解相关推荐

  1. HihoCode1032 最长回文子串 manacher算法

    求最长回文子串的算法比较经典的是manacher算法 转载自这里 首先,说明一下用到的数组和其他参数的含义: (1)p[i] : 以字符串中下标为的字符为中心的回文子串半径长度: 例如:abaa字符串 ...

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

    题目来自lintcode, 链接:http://www.lintcode.com/zh-cn/problem/longest-palindromic-substring/ 最长回文子串 给出一个字符串 ...

  3. 求解最长回文子串----Manacher 算法

    最长回文子串问题:给定一个字符串,求它的最长回文子串长度. 如果一个字符串正着读和反着读是一样的,那么我们称之为回文串.例如:abba.aaaa.abvcba.123321等 暴力法:遍历字符串的所有 ...

  4. 最长回文子串——Manacher 算法​​​​​​​

    0. 问题定义 最长回文子串问题:给定一个字符串,求它的最长回文子串长度. 如果一个字符串正着读和反着读是一样的,那它就是回文串.下面是一些回文串的实例: 12321 a aba abba aaaa ...

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

    #1032 : 最长回文子串 时间限制:1000ms 单点时限:1000ms 内存限制:64MB 描述 小Hi和小Ho是一对好朋友,出生在信息化社会的他们对编程产生了莫大的兴趣,他们约定好互相帮助,在 ...

  6. 【字符串】最长回文子串 ( 蛮力算法 )

    文章目录 一.回文串.子串.子序列 二.最长回文子串 1.蛮力算法 2.时间复杂度最优方案 一.回文串.子串.子序列 " 回文串 ( Palindrome ) " 是 正反都一样的 ...

  7. 回文字符串—回文子串—Manacher算法

    leetcode地址:5. 最长回文子串 解答参考:动态规划.中心扩散.Manacher 算法 问题描述: 给你一个字符串 s,找到 s 中最长的回文子串.比如给定字符串s = "babad ...

  8. manacher java_最大回文子串(Manacher算法)

    1.Manacher算法 首先说明一下,Manacher算法能够使得在O(n)的时间复杂度下找到最长的回文子串. (1).Manacher算法的概述 Manacher算法只能解决长度为奇数的字符串,所 ...

  9. 最长回文子串 -- 马拉松算法

    百度了好长时间,看了很多篇博客才稍微看懂,所以自己写篇博客加深一下映像,并且写的尽量详细一些 希望大家能够只这篇博客就能看懂,能少走些弯路 马拉松算法 1.添加特殊字符 通常情况下,对于一个字符串,需 ...

  10. 【字符串】最长回文子串 ( 动态规划算法 ) ★

    文章目录 一.回文串.子串.子序列 二.最长回文子串 1.动态规划算法 2.动态规划算法代码示例 一.回文串.子串.子序列 " 回文串 ( Palindrome ) " 是 正反都 ...

最新文章

  1. Pytorch使用tensorboardX可视化。超详细
  2. python多线程执行类中的静态方法
  3. C语言位操作--判断整数是否为2的幂
  4. Eclipse配置Android开发环境
  5. springer grammar revise tool
  6. ubuntu下docker的安装及更换镜像源
  7. Spring Boot——HTTP访问重定向到HTTPS解决方案
  8. 五个在线图形工具创建简单的设计元素
  9. 如何在 ASP.Net Core 中使用 条件中间件
  10. java开发可以转什么软件有哪些_转行开发软件Java编程必须会什么
  11. 从需求到交付——论敏捷过程中的需求管理
  12. WDM驱动程序介绍(引)
  13. linux网络系统调用,Linux网络系统调用接口--待续
  14. mysql5.7安装教程(linux)
  15. eclipse换炫酷主题
  16. 计算机的数字符号化是什么,电脑输入数字变成符号怎么办
  17. 日本杂货连锁店Loft首家海外直营店于上海开业
  18. python怎么安装turtle_Turtle模块安装
  19. 推拿师考证需要什么条件
  20. 浅析向上转型和向下转型

热门文章

  1. androidStudio不得不装的21款,Android Studio插件
  2. 宝塔面板部署vue项目到云服务器上(Nginx服务器)
  3. glup安装以及使用
  4. 奇偶分家 (10 分)
  5. python 游戏大作_Linux下游戏大作赏析(二)
  6. JS div匀速移动动画与变速移动动画
  7. 【游戏逆向】一款单机游戏明文包分析
  8. java di是什么_Spring IOC和DI的理解有什么区别
  9. MFC学习(27)CString内存泄漏的雷区——GetBuffer与ReleaseBuffer
  10. C++项目最大收益(贪心问题)