Manacher 算法求最长回文子串
背景
本文来源于leetcode上的一道求最长回文子串的中级算法题
先来个思路
由回文的性质想到,从回文的对称轴向两边同时扩张,在回文半径内,两边的字符一定是相同的。
因此,我们是否可以从左向右依次遍历字符串,用扩展的方法来找出所有的回文串,并记录最长的那一个呢?
说干就干,经过几次WA,终于AC了,代码如下:
1 | func longestPalindrome(s string) string { |
简单估计了下,长度为n的字符串,该思路的时间复杂度应该是O(n^2)级别的,而且,可以看到代码中需要分别处理奇偶长度的字符串,比较麻烦。
最最最关键的是,实在想不出其他的发子了。。。哈哈
manacher
经过一番搜索后,发现该问题大多数思路是使用 manacher算法,这个算法可以完美的解决奇偶问题,并且时间复杂度只有O(n)
解决奇偶单独处理的问题
由奇偶数的性质:奇数+偶数=奇数,因此假设我们的序列长度为奇数,我们在序列首尾和每个字符之间都添加一个字符#,则长度变为奇数,同理,长度为偶数时,如此处理后长度仍为奇数。
由此,通过添加一个额外的字符,就能解决序列长度特殊处理的问题。
算法实际流程
将问题简单化之后,接下来就要进入算法的核心流程了。
首先,manacher需要借助一个辅助数组p[i],用来记录位置为i的字符的回文半径,因而字符串#a#b#a#d的p[i]则为:
1 | 处理后的字符串 | # a # b # a # d # |
可以看到,对于和原始字符串中对应位置的字符所在回文的长度,正好等于处理后的字符串的p[i] - 1
也就是说,只要求得了p[i],就能获得最长的回文串
求p[i]
求p[i]的情况可以分为两种:
- 当前的字符处于未被遍历过的字符串
- 当前的字符处于已经被遍历过的字符串
对于情况1,由于没有历史数据可以借鉴,因此只能直接重新通过双向扩展来求回文半径
而对于情况2,如果我们不利用已经遍历过的回文的性质,那么算法就会和一开始的暴力遍历法一样低效,因此,这里需要仔细考虑如何利用回文性质快速求出会问半径p[i]
利用历史数据快速求解回文半径
假设之前已遍历的回文子串能延伸到达的最远位置为max,其所对应的对称轴为id,那么,对于上文的情况2,当前的字符c一定位于max的左边,并且可以利用回文的性质,通过c关于id的对称点j来求p[i]
因此,我们可以看到,回文半径的求解,分为以下几个情况:
j的回文包含在id的回文内
这种情况下,p[i] = p[j] = p[2*id - i]且p[i] < max - ij的回文包含在id的回文外
这种情况下,j的回文半径已经超出了id的半径,因此,我们需要推断i的回文半径是否也会超出
结论是:不会!
原因:假如j的回文包含a和b,且b等于c(b,c关于id对称),那么如果i的回文超过了max,则超过max的部分必定和j超出id左边回文半径有部分相等,这样,就会导致id的回文半径超出max,和已知条件矛盾,因此这是不可能的
此时,p[i] = max - i
综上所述,当p[i] < max - i时,p[i] = p[2*id - i],而p[i] == max - i时,p[i] == max - i,因此可以得到,`p[i] = min(max - i, p[2*id - i])
最后,上代码
1 | func min(a, b int) int { |