一.问题描述
Longest Palindromic Substring
Given a string S, find the longest palindromic substring in S. You may assume that the maximum length of S is 1000, and there exists one unique longest palindromic substring.
回文字符串,eg:
“abba”或“abcba”
二.编写代码
回文字符串是以最中间字符为中心向两边对称的,因此考虑如下算法:对每个字符串,寻找以其为中心的最长回文子串。该算法时间复杂度为O(N^2)。算法实现中需要注意的是两种回文情况要分别考虑到。
代码如下:
class Solution(object):
def longestPalindrome(self, s):
"""
:type s: str
:rtype: str
"""
# 长度
len_s = len(s)
# 两个指针初始化
# i=0
# j=len_s-1
# 初始化最长回文字符串机器长度
max_str = s[0]
max_str_len = 1
# 奇数长度的回文串
for i in range(0, len_s - 1):
# 对每个i,寻找以它为中心的最长回文串
len1 = 1
j = i + 1
i = i - 1
while i >= 0 and j < len_s:
if s[i] == s[j]:
len1 = len1 + 2
i = i - 1
j = j + 1
else:
break
if len1 > max_str_len:
max_str_len = len1
max_str = s[i + 1:j]
# 偶数长度的回文串
for i in range(0, len_s-1):
# 对每个i,寻找以它为中心的最长回文串
len1 = 1
if s[i] == s[i + 1]:
len1 = len1 + 1
i = i - 1
j = i + 3
while i >= 0 and j < len_s:
if s[i] == s[j]:
len1 = len1 + 2
i = i - 1
j = j + 1
else:
break
if len1 > max_str_len:
max_str_len = len1
max_str = s[i + 1:j]
elif i>1 and s[i] == s[i - 1]:
len1 = len1 + 1
i = i - 2
j = i + 3
while i >= 0 and j < len_s:
if s[i] == s[j]:
len1 = len1 + 2
i = i - 1
j = j - 1
else:
break
if len1 > max_str_len:
max_str_len = len1
max_str = s[i + 1:j]
return max_str
三.最长回文子串--Manacher算法
1)由于回文串的奇偶性造成了不同的对称轴位置,要队两种情况分别处理;
2)很多子串被重复多次访问。
char: a b a b a
i : 0 1 2 3 4
当i==1和i==2时,左边的子串aba被重复访问了。
针对上述问题的分别改进:aba ———> #a#b#a#
abba ———> #a#b#b#a#
char: # a # b # a #
RL : 1 2 1 4 1 2 1
RL-1: 0 1 0 3 0 1 0
i : 0 1 2 3 4 5 6
char: # a # b # b # a #
RL : 1 2 1 2 5 2 1 2 1
RL-1: 0 1 0 1 4 1 0 1 0
i : 0 1 2 3 4 5 6 7 8
我们再引入一个辅助变量MaxRight
,表示当前访问到的所有回文子串,所能触及的最右一个字符的位置。另外还要记录下MaxRight
对应的回文串的对称轴所在的位置,记为pos
,它们的位置关系如下。
我们从左往右地访问字符串来求RL,假设当前访问到的位置为i
,即要求RL[i],在对应上图,i
必然是在po
右边的(obviously)。但我们更关注的是,i
是在MaxRight
的左边还是右边。我们分情况来讨论。
1)当i
在MaxRight
的左边
情况1)可以用下图来刻画:
我们知道,图中两个红色块之间(包括红色块)的串是回文的;并且以i
为对称轴的回文串,是与红色块间的回文串有所重叠的。我们找到i
关于pos
的对称位置j
,这个j
对应的RL[j]
我们是已经算过的。根据回文串的对称性,以i
为对称轴的回文串和以j
为对称轴的回文串,有一部分是相同的。这里又有两种细分的情况。
-
以
j
为对称轴的回文串比较短,短到像下图这样。
这时我们知道RL[i]至少不会小于RL[j],并且已经知道了部分的以i
为中心的回文串,于是可以令RL[i]=RL[j]
。但是以i
为对称轴的回文串可能实际上更长,因此我们试着以i
为对称轴,继续往左右两边扩展,直到左右两边字符不同,或者到达边界。
-
以
j
为对称轴的回文串很长,这么长:
这时,我们只能确定,两条蓝线之间的部分(即不超过MaxRight的部分)是回文的,于是从这个长度开始,尝试以i
为中心向左右两边扩展,,直到左右两边字符不同,或者到达边界。
不论以上哪种情况,之后都要尝试更新MaxRight
和pos
,因为有可能得到更大的MaxRight。
具体操作如下:
step 1: 令RL[i]=min(RL[2*pos-i], MaxRight-i)
step 2: 以i为中心扩展回文串,直到左右两边字符不同,或者到达边界。
step 3: 更新MaxRight和pos
2)当i
在MaxRight
的右边
遇到这种情况,说明以i
为对称轴的回文串还没有任何一个部分被访问过,于是只能从i
的左右两边开始尝试扩展了,当左右两边字符不同,或者到达字符串边界时停止。然后更新MaxRight
和pos
。
本人实现代码如下:(注:代码还可优化,没必要分这么多情况,比如最左边界只用取最小值即可)
class Solution(object):
def longestPalindrome(self, s):
"""
:type s: str
:rtype: str
"""
# 预处理,插入特殊字符
s = '#' + '#'.join(s) + '#'
# 长度
len_s = len(s)
# 回文半径初始化
RL = [0] * len_s
# 当前最靠右的回文子串的相关信息,left和right关于pos对称
left = 0
right = 0
pos = 0
# 初始化最大长度回文子串
max_str = s[1]
max_str_len = 1
# 依次求RL[i]
for i in range(len_s):
# step1:求RL[i]
# 第一种情况:i在right的右边,即其相应的回文子串并没有被访问过
if i >= right:
# 定义start为继续扩展回文子串的地方(关于i,从start开始向左)
RL[i]=1
start = i - 1
else:
# i关于pos的对称位置j
j = 2 * pos - i if pos != 0 else 0
# 第二种情况:j及其回文串均在left到right内
if (j - RL[j]) >= left:
RL[i] = RL[j]
start = i - RL[i]
# 第三种情况:j在left到right内,但其回文子串已经超出该范围
else:
RL[i] = right - i + 1
start = 2 * i - right - 1
while start >= 0 and (2 * i - start) < len_s and s[start] == s[2 * i - start]:
RL[i] += 1
start=start-1
# step2:更新最靠右回文子串的相关信息
if (i + RL[i] - 1) > right:
right = i + RL[i] - 1
pos = i
left = 2 * pos - right
# 更新最长回文子串信息
if (RL[i] - 1) > max_str_len:
max_str_len = RL[i] - 1
max_str = s[i - RL[i] + 1:i + RL[i]]
return max_str.replace('#', '')
算法复杂度分析:
时间复杂度:尽管代码里面有两层循环,通过amortized analysis我们可以得出,Manacher的时间复杂度是线性的。由于内层的循环只对尚未匹配的部分进行,因此对于每一个字符而言,只会进行一次,因此时间复杂度是 O(n) 。