最长回文子串
- 我的解法:这道题很快的就写出了答案,唯一不足的是时间复杂度太高了:
def longestPalindrome(s):
if len(s) <= 1:
return s
l = list()
length = len(s)
for i in range(0, length):
start = i
j = length-1
end = j
while i < j:
if s[i] == s[j]:
j -= 1
i += 1
else:
i = start
j = end - 1
end = j
if len(s[start : end+1]) > 1:
l.append(s[start : end+1])
#print(l)
max_index = 0
for i in range(0, len(l)):
if len(l[i]) > len(l[max_index]):
max_index = i
if len(l) == 0:
return s[0]
return l[max_index]
print(longestPalindrome(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
aaaabcaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))
- 对于上面代码的理解,找一个实例在纸上走一遍就知道了。不再赘述。【提示:滑动窗口——类似于:https://blog.csdn.net/jy_z11121/article/details/101381983】
- 这个方法相当于是“遍历法”,可以说几乎任意两个字符都做了比较…
- 复杂度分析(空间复杂度为1表明只需要常数个变量):
- 另一种暴力解法:
def isPalindromic(test_s): # 判断test_s字符串是否为回文,是的话则返回True,否则False
length = len(test_s)
for i in range(0, int(length/2)):
if test_s[i] != test_s[length - i - 1]:
return False
return True
def longestPalindrome(s):
longest = 0
length = len(s)
for i in range(0, length):
for j in range(i+1, length+1):
test_s = s[i:j]
if isPalindromic(test_s) and len(test_s) > longest:
ans = test_s
longest = len(test_s)
return ans
print(longestPalindrome("aabcddcba"))
- 思路:从头向尾循环出所有字符串,判断其是否为回文。
- 此暴力解法与上一解法相比,会更慢一点。
改进:最长公共子串
-
思路:
因为回文字符串指的是“正着读与倒着读相同”,所以我们可以考虑把原来的字符串倒置,然后找原字符串与倒置后的字符串的最长公共子串。例如 S = “caba” ,S` = “abac”,最长公共子串是 “aba”,所以原字符串的最长回文串就是 “aba”。 -
实现:
- 两个字符串最长公共子序列的找出有多种方法(这也是一个很重要的算法题,如涉及到的KMP算法),这里使用动态规划的方法来找出这是最长公共子序列,也就是原字符串的最长回文序列。
- 实现动态规划用到的“备忘录”是一个“一维数组”(列表实现)。这个备忘录要记录什么内容呢?(只要能想清楚这个,就好说了!)这个备忘录应该记录公共子序列的长度。如何实现这一记录过程呢?循环(正向循环)每个原字符串,其循环体里将循环(逆向循环)倒置后的字符串,使其分别与原字符串中的每个字符进行比较,根据每次的比较结果(两字符是否相等)来更新这个备忘录。为什么需要逆向循环呢?因为我们需要用到上一轮的结果(
arr[j] = arr[j-1] + 1
)!如果正向循环的话,我们就无法使用上一轮的更新结果了,使用到的将是本轮新更新的结果。
- 这个备忘录实际上具有两个意义,一是记录原字符串中的字符s[i-1]与倒置字符串的每个字符比较的结果,二是记录原字符串中的字符s[i]与倒置字符串的每个字符比较的结果。当要更新i(即i即将进行一次
i++
操作时)时,此时备忘录里,只有“原字符串中的字符s[i-1]与倒置字符串的每个字符比较的结果”;当同一轮i下,仅仅只是在更新j时,则备忘录里既含有“原字符串中的字符s[i-1]与倒置字符串的每个字符比较的结果”,也含有“原字符串中的字符s[i]与倒置字符串的每个字符比较的结果”。请参考下图。
- 然而,这种方法来寻找回文序列是有缺陷的。如:当 S = "abc435cba"时 ,S` = “abc534cba”,此时,将判断出最长公共子序列为“abc”和“cba”,但它们在原字符串里并不是回文序列!所以,我们应有所判断。那么,如何排除这种情况呢?答案就是再进行一个判断:如果判断出来的公共子序列真的是原字符串里的回文序列,那么一定有一些规矩,具体请看代码吧。
- 代码如下:
def longestPalindrome(s):
length = len(s)
arr = [0] * length # 创建“备忘录”
#print(arr)
rev_s = ''.join(reversed(s)) # s的倒置字符串
#print(rev_s)
maxLen = 0
maxEnd = 0
for i in range(0, length):
for j in range(length-1, -1, -1):
if s[i] == rev_s[j]:
if i == 0 or j == 0:
arr[j] = 1
else:
arr[j] = arr[j-1] + 1
else:
arr[j] = 0
# 判断是否真的是回文序列
if arr[j] > maxLen:
beforeRev = length - j - 1 # 该字符在原字符串中的索引
if beforeRev + arr[j] - 1 == i:
maxLen = arr[j]
maxEnd = i
return s[maxEnd-maxLen+1 : maxEnd+1]
print(longestPalindrome("abad"))
碎碎念:对于我来说,不好理解,也更不好想到,花了大概两个小时才差不多想明白…好复杂,而且也没什么太强的规则性。。。学着了解怎么样的情况下需逆向循环吧。
改进:动态规划
def longestPalindrome(s):
if len(s) <= 1:
return s
length = len(s)
l = list() # 列表l用来存放所有回文
# 初始化一字母和二字母的回文
for i in range(0, length):
l.append([i, i])
if i + 1 <= length - 1:
l.append([i, i+1])
#print(l)
# 根据一字母和二字母的回文找到三字母回文、四字母回文...
for i in range(0, length):
for j in range(0, length):
if s[i] == s[j] and ([i+1, j-1] in l):
l.append([i, j])
# print(l)
end_l = l[len(l)-1]
if end_l[1] - end_l[0] == 0: # 最长回文就是相邻两字符的情况
for xl in l:
if xl[0] != xl[1] and s[xl[0]] == s[xl[1]] : # 相邻两字符真的是回文的情况
ret_l = xl
break
else: # 所有相邻两字符都并不是回文的情况——返回s的首字符
ret_l = l[0]
else:
# 找出最长回文
max_l = l[0]
for xl in l:
if xl[1]-xl[0] > max_l[1]-max_l[0]:
max_l = xl
ret_l = max_l
return s[ret_l[0] : ret_l[1]+1]
print(longestPalindrome("abcba"))
可以看到,上面的运行结果是错误的,因为“根据一字母和二字母的回文找到三字母回文、四字母回文…”这一块的代码有问题,应该“从内向外”找,否则会先判断最外侧的两个“a”是否符合回文关系,根据条件,列表 l
中还没有能使它得出“是回文”结果的内容,所以就没有被保存下来。然而,就算“从内向外”找,对于这个for循环来说,它也并不认得在这个for循环里append()的内容,仍然是按照最开始找到的一二字母的回文来执行for循环代码块中。。。就不知道如何解决了。。。
但是可以使用C或Java的二维数组来实现,具体请看:https://leetcode-cn.com/problems/longest-palindromic-substring/solution/xiang-xi-tong-su-de-si-lu-fen-xi-duo-jie-fa-bao-gu/
改进:中心扩展
- 思路:
我们知道回文串一定是对称的,所以我们可以每次选择一个中心,进行左右扩展,判断左右字符是否相等即可。由于回文序列不是奇数个就是偶数个,因此“中心扩展”的"中心"既可能是一个字符(如“aba”的中心是“a”),也可能是一个假象的分割线(如“aaaa”的中心)。也因此,对于给定的一个字符串来说,总共有 [ 2 ∗ l e n ( S ) − 1 ] [2*len(S)-1] [2∗len(S)−1] 个中心。
- 实现:
def expandAroundCenter(s, left, right):
L, R = left, right
while L >= 0 and R < len(s) and s[L] == s[R]:
L -= 1
R += 1
return R - L - 1
def longestPalindrome(s):
if len(s) <= 1:
return s
start, end = 0, 0
length = len(s)
for i in range(0, length):
len1 = expandAroundCenter(s, i, i) # 以一个字符为中心进行扩展
len2 = expandAroundCenter(s, i, i) # 以假象的分割线为中心进行扩展
longest = max(len1, len2)
if longest > end - start:
start = i - (longest-1) // 2
end = i + longest // 2
return s[start : end+1]
print(longestPalindrome("abcba"))
碎碎念:不管是思路还是代码都挺好理解的,但是并不容易去想出它的代码实现…
改进:Manacher’s Algorithm 马拉车算法
Manacher’s Algorithm由一个叫 Manacher 的人在 1975 年发明的,这个方法的最大贡献是将此问题的时间复杂度降到了线性 O ( n ) O(n) O(n)。下面进行该算法的讲解:
思路
- 对原字符串
S
进行处理
- 为了使得扩展的过程中,到边界后自动结束,在两端分别插入 “^” 和 “$”——两个不可能在字符串中出现、且不相同的字符。这样中心扩展的时候,判断到边界的两端字符是否相等的时,答案是否,从而退出循环。
- 在原字符串的每两个字符间插入字符“#”。假设原
S
为abcbaade
,则进行上面的两个处理后,得到新的字符串T
:
进行了上面两个操作后,就可以保证所给字符串的长度一定是奇数了。为什么要作如此“保证”呢? 因为这样就可以不论原字符串是奇数个字符还是偶数个字符,都可以统一进行处理了,原字符串中的每个字符都能进行后面需要的计算。
- 构造一个有意义的数组
P
- 依次以字符串
T
中的每个字符作为中心进行扩展,用一个数组P
保存最大扩展出的有意义的字符个数(“有意义的字符”指的是原字符串S
中的字符)。
示例:对于下图中的字符串
T
——
T[0]对应的P为1,因为只有"#c#"
T[6]对应的P为5,因为扩展得到的内容是"#c#b#c#b#c"
T[11]对应的P为2,因为扩展得到的内容是“#c#c#”
- 得出最终结果——最大回文子串
- 数组
P
中元素值最大的那个,就是我们想要的回文子串。例如,这里的P[6]最大,所以我们希望返回“cbcbc”。规律在于:用 P 的下标 i 减去 P [ i ],再除以 2,就是回文子串的首字符下标了。例如我们找到 P[ i ] 的最大值为 5,也就是回文串的最大长度是 5,对应的下标是 6,所以原字符串的开头下标是(6 - 5 )/ 2 = 0。所以我们只需要返回原字符串的第 0 到 第(5 - 1)位就可以了。
求数组P
这是算法的关键,它充分利用了回文串的对称性,从而降低了时间复杂度。
在上面的思路讲解时的第二步,我们知道了数组P的存在。那么,如何将其用程序求出来呢?
如果用上面已经讲到的中心扩展法,那就向两边扩展比对就行了,但是这样的话这个算法就没有啥意义了,因为就相当于上面的中心扩展法了,反而还搞得复杂了一点。所以,我们这里有新的idea——利用回文的对称性。
- 想象你在"abaaba"中心画一道竖线,你是否注意到这个字符串围绕此竖线是中心对称的?再试试"aba"的中心,这个字符串围绕此中心也是对称的。这当然不是巧合,而是在某个条件下的必然规律。我们将利用此规律减少对数组P中某些元素的重复计算。
- 找一个字符串中的最长回文子串时,最坏的情况是各个回文相互重叠的时候(例如"aaaaaaaaaa"和"cabcbabcbabcba"),这是因为这种情况会发生重复计算。换句话说,没有重叠时,只能一点一点计算,也就没有可改进的余地了。
- 于是我们就可以考虑:花费一些空间来避免重复计算 → 利用回文的特性避免重复计算
假设有一个具有重叠因子的字符串S: babcbabcbaccba
,对其进行转换得到字符串T:
C 表示回文串回文"abcbabcba"的中心,L、R 分别表示回文串的左右边界。假设你已经算出了一部分 P,你下一步要计算P[i],i 围绕 C 的对称点是 i’ 。你有办法高效地计算 P[i] 吗?
我们先看一下 i 围绕 C 的对称点 i’(此时 i’ = 9):
据上图所示,很明显 P[i] = P[i’] = 1
。这是因为 i 和 i’ 围绕 C 对称。同理,P[12] = P[10] = 0
,P[14] = P[8] = 0
。
然而,P[15] != (P[7] = 7)
(P[15] = 5)!这是为什么呢?
如上图所示,两条绿色实线划定的范围必定是对称的,两条绿色虚线划定的范围必定也是对称的。此时请注意 P[i’] = 7,然后我们找到以 P[i] 为中心的长度为7的回文串,发现这个范围超过了左边界L(超出的部分不再对称)。根据这个回文串所占的绿色区域,我们能认为 P[i] >= 5,至于 P[i] > 5 是否成立,还要通过后面的(绿色范围外的)逐个字符检测才能判定出来。在此例中,我们判断到 P[21] ≠ P[9],所以P[i] = P[15] = 5。
对于上面的讲述作总结,即:
if P[ i' ] < R – i :
P[ i ] ← P[ i' ]
else:
P[ i ] ≥ R - i (此时要从R开始逐个字符求P[i],并更新C及其R)
很明显,C的位置也需要变动。它的变化规律是:如果i处的回文超过了R,那么就C=i,同时相应改变L和R即可。
对此算法的完整流程进行总结(PS. 下面的说过程应该比对着代码以及自己做出图示来理解):
- P 从索引 i 为 1 处开始求,并初始化 C、R 都为 0;
- 每一轮都应该先求出 i’ ,即 i 关于 C 的对称索引(在代码中用 i_mirror 来表示):i_mirror = C - (i - C) ;
- 每一次也还都要判断当前要求的 P 的索引 i 是否出了对称区域,即 i > R 的话,就出了区域——我们用 diff 表示 (R - i):
- 如果 diff >= 0 的话, 就说明仍在对称区域内,则可以利用回文串的对称性来求 P[i] : 根据上面讲到的,这里还不能直接另 P[i] = P[i_mirror],因为可能 P[i_mirror] 的回文长度超出了我们设定在与 R 有关的那个范围内,这种情况下,继续比对超过对称范围外的 P[i] 两侧的字符,并改变 C 和 R。
- 如果 diff < 0 的话,就说明 i > R,说明前面没判断到对称的部分,所以 P[i] = 0,同时继续判断其两边的字符是否相等,如果相等,则 ++P[i],同时改变C和R。
- 最后从 P[i] 里选择出最大值,作出相应截取就行了。
def preProcess(s):
length = len(s)
if length == 0:
return "^$"
ret = "^"
for i in range(length):
ret += '#' + s[i]
ret += "#$"
return ret
def longestPalindrome(s):
t = preProcess(s)
length = len(t)
p = [-1] * length
c, r = 0, 0
for i in range(1, length-1):
i_mirror = c - (i - c)
diff = r - i
if diff >= 0: # 当前i在C和R之间,可以利用回文的对称属性
if p[i_mirror] < diff: # i的对称点的回文长度在C的大回文范围内部
p[i] = p[i_mirror]
else:
p[i] = diff
# i处的回文可能超出C的大回文范围了
while t[i + p[i] + 1] == t[i - p[i] - 1]:
p[i] += 1
c = i
r = i + p[i]
else:
p[i] = 0
while t[i + p[i] + 1] == t[i - p[i] - 1]:
p[i] += 1
c = i
r = i + p[i]
maxLen = 0
centerIndex = 0
for i in range(1, length-1):
if p[i] > maxLen:
maxLen = p[i]
centerIndex = i
return s[(centerIndex-1-maxLen)//2 : (centerIndex-1-maxLen)//2+maxLen]
print(longestPalindrome("babcbabcbaccba"))