将近期刷LeetCode题目时的一些心得与总结与大家分享一下。
这次,将集中整理最长回文子串、最长回文子序列的问题。
解决问题
给定一个字符串,要求求出这个字符串中的最长的回文串子串。此处以最长回文子串为例进行讲解,最后给出最长回文子序列的求解思路与方法。
注:最长回文子串与最长回文子序列是不同的,回文子串要求所求的字符串连续,最长回文子序列,则不要求。因此,也导致两者求解思路的不同。
例如,对‘abcda’, 最长回文字符串长度为1,任意一个字符均可;而最长回文子序列长度为3,即a(b,c,d)a。(b,c,d)表示任选一个字符
求解思路一:采用暴力求解
自前至后求解所有子串,并判断相应的子串是否为回文子串,找出其中最长的那个。
求每一个子串时间复杂度O( ), 判断子串是不是回文O(n),两者是相乘关系,所以时间复杂度为O( )。
求解思路二:动态规划
对于字符串问题,常用的一种思路是动态规划的方式。主要思路来源于对于思路一的优化。在思路一中,是将所有的字符串进行了组合,再判断。一种可以想到的简化思路时,在组合字符串的过程中,动态处理,只保留可以组成回文字符串的,而不必将每种组合都进行处理。从而节省时间,也节省了最后判断是否为回文子串的过程。
引入DP[i][j],表示第i个字符至第j个字符组成的子串是否为回文子串。
则状态转移方程为:
注:对于长度为2的情况,即i+1=j时,实质上不存在DP[i+1][j-1]。此时j- 1 > i+1,所以需单独处理一下。其实,也可以通过将数组初始化为1来求解,这样不需要判断(详细见下方源码)
在使用动态规划求解时,需保证计算当前数组中的值时,则需要判断相应的遍历规则。
以字符串‘abca’为例:能确定出值的,为下表中已填入数字的部分。
a | b | c | a | |
---|---|---|---|---|
a | 1 | |||
b | 1 | 1 | ||
c | 1 | 1 | 1 | |
a | 1 | 1 | 1 | 1 |
根据上述状态转移方程,我们可以的填充顺序为:
(1,2)->(2,3)->(3,4)->(1,3)->(2,4)->(1,4)
即按照斜线,自对角线开始逐渐向右上角填充;
或者自下而上、自左至右依次填充。即行遍历由下至上,列遍历由左至右的方式。
综上,代码如下:
class Solution:
def longestPalindrome(self, s):
"""
:type s: str
:rtype: str
"""
if not s:
return ""
n = len(s)
# 动态规划算法
dp = [[1 for _ in range(n)] for _ in range(n)]
sta = 0
end = 0
max_l = 1
for i in range(n - 1, -1 , -1):
dp[i][i] = 1
for j in range(i + 1, n):
dp[i][j] = (s[i] == s[j]) & dp[i + 1][j - 1]
# 保留最大值,基于上述遍历规则,此处保留的为最右端的最长子串;如果求最左端的,将下述的<变为<=即可
if dp[i][j] and max_l < j - i + 1:
max_l = j - i + 1
sta = i
return s[sta:max_l + sta]
求解思路三:Manacher算法(马拉车算法)
这是网上提供的线性时间算法。
以字符串‘abca’为例。
S1:首先用特定字符,比如"#",去填充原来的字符串s,从而将长度n变为2n+1。
“#a#b#c#a#”
S2:引入数组A, A[i]表示以新字符串中第i个字符为中心的最长回文子串的长度(只须记录往一侧扩展的值即可,计算方便)
A[0]: 1
A[1]: 3 “#a#”
…
S3:找出数组A中最大的值,并从中心点截取前后max(Len)-1长度,再去掉“#"即为最后所要求的字符串。
class Solution:
def longestPalindrome(self, s):
"""
:type s: str
:rtype: str
"""
if not s:
return ""
n = len(s)
temp_s = ['#']
for i in range(n):
temp_s.append(s[i])
temp_s.append('#')
out_len = []
for i in range(2 * n + 1):
temp_len = 1
try:
while temp_s[i - temp_len] == temp_s[i + temp_len]:
temp_len += 1
except IndexError:
pass
out_len.append(temp_len)
max_len = max(out_len)
max_len_index = out_len.index(max_len)
out = temp_s[max_len_index - max_len + 1:max_len_index + max_len]
res = ""
for each in out:
if each != '#':
res += each
return res
扩展问题:求回文子序列的个数
注:未考虑子序列的重复问题
如果一个字符串为回文子序列,则去掉前后各1个字符后,仍为回文子序列。
求解思路:基于动态规划
引入DP[i][j],表示第i个字符至第j个字符组成的子串中回文子串的数量。
此时的状态转移方程:
代码中,需要注意,DP要初始化为0。
最长回文子序列问题
求解思路:基于动态规划
引入DP[i][j],表示第i个字符至第j个字符组成的子串中最长回文子序列的长度。
此时的状态转移方程: