最长回文子串算法——Manacher算法

最长回文子串算法——Manacher算法

Manacher算法是一个用来查找一个字符串中的最长回文子串(不是最长回文序列)的线性算法。其优点就是把时间复杂度从暴力算法的O(n2)优化到O(n)

Manacher 算法,又被中国程序员戏称为“马拉车”算法

暴力匹配算法

暴力匹配算法的原理

暴力匹配算法的原理很简单,如下:

  1. 依次向尾部进行遍历,访问一个字符;
  2. 以此字符为中心点向两边扩展,记录该点的最长回文长度;
  3. 取各个字符的回文子串长度的max

暴力匹配算法存在的问题

  1. 偶数回文串需要额外修改

    在奇数字符串中,例如 aba,对应的回文长度是 131。而例如abba,以使用中心扩展的比较原则下计算出来的回文长度是 1111,我们对奇数回文串求出了正确答案,但是在偶数回文串上并没有得到我们想要的结果,需要针对偶数情况进行额外修改。

  2. 时间复杂度O(n2)

    外层需要遍历每一个字符,而每到一个新字符就需要向两边扩展比对,所以时间复杂度达到了O(n * n)。

Manacher算法本质上也是基于暴力匹配的方法,只不过做了一点简单的预处理,且在扩展时提供了加速

Manacher算法的预处理

Manacher算法对偶数字符串做了预处理,这个预处理可以巧妙的让所有(包括奇和偶)字符串都变为奇数回文串。操作实现也很简单,就是将原字符串的首部尾部以及每两个字符间插入一个特殊字符(假设为#号),这个字符不会影响最终的结果,这一步预处理操作后的效果就是原字符串的长度从n改变成了2n + 1。比如我们的原字符串是 abba,假设预处理后的字符串是 #a#b#b#a#,我们在任意一个点,比如字符 #,向两端匹配只会出现原始字符匹配原始字符,#匹配 # 的情况,不会出现原字符串字符特殊字符匹配的情况,这样就能保证我们不会改变原字符串的匹配规则。该预处理得到进行下一步扩展的字符串,并且从预处理后的字符串得到的最长回文字符串的长度除以2就是原字符串的最长回文子串长度,也就是我们想要得到的结果。

Manacher算法核心

概念

  • ManacherString:经过Manacher预处理的字符串,以下的概念都是基于ManasherString产生的。

  • 回文半径:经过处理后的字符串的长度一定是奇数,回文半径就是以回文中心字符的回文子串长度的一半。

  • 回文直径: * 2 − 1

  • 最右回文边界R:遍历字符串时,每个字符的最长回文子串都会有个边界,而R则是所有已知右边界中最右的位置。R值保持单增。

  • 回文中心C:取得当前R的上一次更新时候的回文中心。

  • 半径数组P[]:该数组记录原字符每一个字符对应的最长回文半径。

算法流程

步骤1:将原字符串转换为ManacherString,定义为S

步骤2RC的初始值为−1,创建半径数组P[]

  • 存在与概念相差的小偏差,R实际是最右边界位置的右一位。

步骤3:开始从下标i = 0len(S) − 1,去遍历字符串S

后续存在多个分支,总览如下:

分支1:当 i > R 时,暴力匹配当前i位置字符的最长回文长度,并判断更新R和C。例如aabcdi = 0R = −1 时,初次更新R = 1i = 0字符a的最长回文右边界下标0的右一位),C = 0

分支2i ≤ R ​时,也就是说当前i下标的字符已经在某个字符的回文半径覆盖中,该分支存在三种情况,解释三种情况前,需要先理解以下模型:

L是当前R关于C的对称点,ii关于C的对称点,因此i = 2 * C − i,并且因为从左至右遍历i,所以i的回文区域是前面已知的(信息保存在半径数组P[i]中)。我们可以依赖该信息判断是否进行加速。

情况1i的回文区域在$\overline{LR}$的内部,因为整个$\overline{LR}$就是一个回文串,我们可以直接得出i的回文直径与i相同。

情况2i的回文半径左边界超过L,这种情况,仅能保证i的回文半径是iR

情况3i的回文区域左边界恰好和L重合,此时i的回文半径至少是iR,并且回文区域从R继续向外部匹配。

时间复杂度

Manacher算法时间复杂度为O(n)。我们可以想象下,这就是一个 i在追逐R的游戏,无非两种情况:

  1. R不动,同时i向右追一步。(花O(1)时间计算P[i]
  2. R继续往右走几步,同时i追上一步。(所谓更新R,更新C

R到达最右边界以后,就剩下i一步一步追上来。

因此,每个字符最多被访问两次,一次被R经过,一次被追赶的i经过。所以时间复杂度是O(2 * (2n + 1)) = O(n)

代码

代码引用自我的github仓库地址:longest-palindromic-substring.go

有关题目可参考Leetcode题目: Longest Palindromic Substring