Title
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
示例 1:
输入: “babad”
输出: “bab”
注意: “aba” 也是一个有效答案。
示例 2:
输入: “cbbd”
输出: “bb”
Solve
Manacher
算法
Manacher
算法是用来查找一个字符串最长回文子串的线性方法,这个方法最大的贡献就在于将时间复杂度提升到了线性。
- 字符串预处理
回文串的判定跟中间位置字符相关,如果字符串长度是奇数的话,中间位置字符只有一个,如果字符串长度是偶数的话,中间位置字符有两个,因此,我们需要对字符串进行预处理。
在相邻两个字符间插入一个新的字符'#'
,为了让扩展过程中到边界后自动结束,在字符串的开头和结尾分别插入'^'
和'$'
,当然要确保插入的字符不可能在原始字符串中出现,这样在中心扩展判断两端字符是否相等的时候,到了边界就一定不会相等,从而跳出循环。
经过处理,字符串的长度永远都是奇数了。
- 中心扩展
我们知道回文串一定是对称的,所以我们可以每次循环选择一个中心,进行左右扩展,判断左右字符是否相等即可。
我们用一个数组P保存从中心扩展的最大个数,而它刚好也是去掉'#'
的原字符串的总长度。
例如下图中下标是6的位置,可以看到P[6]=5,所以它是从左边扩展5个字符,右边也扩展5个字符,即'#c#b#c#b#c#'
,去掉'#'
恢复到原来的字符串,变成'cbcbc'
,它的长度刚好也就是5.
- 求原字符串起始下标
公式:用 P 的下标 i 减去 P [ i ],再除以 2。
例如我们找到 P[ i ] 的最大值为 5,也就是回文串的最大长度是 5,对应的下标是 6,所以原字符串的开头下标是(6 - 5 )/ 2 = 0。所以我们只需要返回原字符串的第 0 到 第(5 - 1)位就可以了。
- 求P[i]
充分利用回文串的对称性,就可以求出P[i]的值。
我们用C表示回文串的中心,用R表示回文串的右边半径,即R=C+P[i]
。
C和R所对应的回文串是当前循环中R最靠右的回文串。
以下图为例:
求P[i]的时候,用i_mirror表示当前需要求的第i个字符关于C对应的下标。
如果现在要求 P [ i ],使用中心扩展法向两边扩展比对就行了。但是我们其实可以利用回文串 C 的对称性。i 关于 C 的对称点是 i_mirror,P [ i_mirror ] = 3,所以 P [ i ] 也等于 3。
但是有三种情况将会造成直接赋值为 P [ i_mirror ] 是不正确的,下边一一讨论。
- 超出了 R
当我们要求 P [ i ] 的时候,P [ mirror ] = 7,而此时 P [ i ] 并不等于 7,为什么呢,因为我们从 i 开始往后数 7 个,等于 22,已经超过了最右的 R,此时不能利用对称性了,但我们一定可以扩展到 R 的,所以 P [ i ] 至少等于 R - i = 20 - 15 = 5,会不会更大呢,我们只需要比较 T [ R+1 ] 和 T [ R+1 ]关于 i 的对称点就行了,就像中心扩展法一样一个个扩展。
- P [ i_mirror ] 遇到了原字符串的左边界
此时P [ i_mirror ] = 1,但是 P [ i ] 赋值成 1 是不正确的,出现这种情况的原因是 P [ i_mirror ] 在扩展的时候首先是 “#” == “#”,之后遇到了 “^” 和另一个字符比较,也就是到了边界,才终止循环的。而 P [ i ] 并没有遇到边界,所以我们可以继续通过中心扩展法一步一步向两边扩展就行了。
- i 等于了 R
此时我们先把 P [ i ] 赋值为 0,然后通过中心扩展法一步一步扩展就行了。
- 考虑 C 和 R 的更新
就这样一步一步的求出每个 P [ i ],当求出的 P [ i ] 的右边界大于当前的 R 时,我们就需要更新 C 和 R 为当前的回文串了。因为我们必须保证 i 在 R 里面,所以一旦有更右边的 R 就要更新 R。
此时的 P [ i ] 求出来将会是 3,P [ i ] 对应的右边界将是 10 + 3 = 13,所以大于当前的 R,我们需要把 C 更新成 i 的值,也就是 10,R 更新成 13。继续下边的循环。
复杂度分析
时间复杂度:O(n),其中 n 是字符串的长度。由于对于每个位置,扩展要么从当前的最右侧臂长 right 开始,要么只会进行一步,而 right 最多向前走 O(n) 步,因此算法的复杂度为 O(n)。
空间复杂度:O(n),我们需要 O(n) 的空间记录每个位置的臂长。
Code
def longestPalindrome_manacher(self, s: str) -> str:
def expand(s, left, right):
while left > -1 and right < len(s) and s[left] == s[right]:
left -= 1
right += 1
return (right - left - 2) // 2
end, start = -1, 0
s = '#' + '#'.join(list(s)) + '#'
arm_len = []
right = -1
j = -1
for i in range(len(s)):
if right >= i:
i_sym = 2 * j - i
min_arm_len = min(arm_len[i_sym], right - i)
cur_arm_len = expand(s, i - min_arm_len, i + min_arm_len)
else:
cur_arm_len = expand(s, i, i)
arm_len.append(cur_arm_len)
if i + cur_arm_len > right:
j = i
right = i + cur_arm_len
if 2 * cur_arm_len + 1 > end - start:
start = i - cur_arm_len
end = i + cur_arm_len
return s[start + 1:end + 1:2]