LeetCode 647.回文子串:
思路:
首先分析题目,要求的子串是连续子串
还是使用动态规划。最开始会想到定义dp[i]为结尾为s[i]的回文子串的个数,但是发现dp[i]和dp[i - 1]没有找到依赖关系,那么从回文串的定义出发,定义类似递归的。(如下图)
s[i, j]是否为回文串,首先判断s[i] =? s[j],再判断s[i + 1, j - 1]是否为回文串。因此定义二维dp数组(外面没有多围一圈)
动规五部曲
- ① dp数组及含义:
dp[i][j]:子串s[i, j](左闭右闭)是否为回文子串,是否是dp[i][j] - ② 递推式:
首先依据s[i] = ? s[j]来分类
!= 那么确定dp[i][j] = False
== 那么还需要继续分类。本来按照上面的是判断s[i + 1, j - 1]是否为回文串的,但是首先要判断该子串是否存在,即j - 1 >= i + 1→ j >= i + 2。同时依据dp数组定义,j >= i- j -i >= 2,即 j 与 i 相差大于1,dp[i][j] = dp[i + 1][j - 1],由于题目要统计回文串的个数,所以变成
if dp[i + 1][j - 1] dp[i][j] = Ture
- j - i == 1,即类似aa的这种,dp[i][j] = True
- j == i,同一个数,或者说一个数就是回文串,dp[i][j] = True
- j -i >= 2,即 j 与 i 相差大于1,dp[i][j] = dp[i + 1][j - 1],由于题目要统计回文串的个数,所以变成
- ③ 初始化
初始化为False,虽然dp[i][j]由dp[i + 1][j - 1]推导得到,但是dp[i + 1][j - 1]可以在遍历时推导得到,因此全部初始化为False - ④ 遍历顺序
dp[i][j]由下图蓝色部分推导得到,因此只要遍历顺序在遍历到dp[i][j]时,蓝色部分已经被赋值即可,那么就需要逆序遍历行,顺序遍历列,而遍历行列先后无所谓。以先行后列为例
- ⑤ 举例
由dp数组定义可知,要求 j >= i,因此遍历赋值时只将矩阵右上部分,即下图蓝色部分赋值即可,同时因为要统计数目,因此统计蓝色部分True的个数即为回文串的个数
代码
class Solution:
def countSubstrings(self, s: str) -> int:
lens = len(s)
if lens <= 0:
return 0
# 初始化
dp = [[False] * lens for _ in range(lens)]
result = 0 # 记录回文串个数
# 遍历,遍历行为逆序,列为顺序
for i in range(lens - 1, -1, -1): # 从最后一行开始
for j in range(i, lens):
if s[i] == s[j]:
if j - i <= 1 or (j - i > 1 and dp[i + 1][j - 1]):
dp[i][j] = True
result += 1
return result
LeetCode 516.最长回文子序列:
思路:
首先分析题目,本题要求的是找到最长的回文子序列,返回其长度,且子序列为非连续子序列。
还是可以借助上题的思路,dp[i][j]保存s[i, j]中最长回文子序列的长度。(也就是从串整体考虑)
动规五部曲:
- ① dp数组及含义:
dp[i][j]:si, j中最长回文子序列的长度 - ② 递推式
首先根据s[i] =? s[j]来分类
如果等于,那么最长的长度 = dp[i + 1][j - 1] + 2,根据[i + 1, j - 1]这个区间是否存在来分类- j >= i + 2:dp[i][j] = dp[i + 1][j - 1] + 2
- j = i + 1:如aa,dp[i][j] = 2(这个也就是dp[i][i+1]=dp[i+1][i] + 2,因此也算入上面的公式中)
- j == i:dp[i][j] = 1(这个要额外注意)
如果不等于,又由于 j >= i,那么一定 j >= i + 1(因为j == i 的时候 s[i]一定等于 s[j]),所以下面求的时候不用考虑删除一个元素后剩下的部分为空序列的情况。那么就考虑删除s[i]或s[j]之后再求最长回文子序列了,如下图
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])
- ③ 初始化
dp数组保存的是长度,因此全部初始化为0 - ④ 遍历顺序
dp[i][j]由下面蓝色部分推导得到,从行的角度看,i 由 i 和 i + 1推导得到,因此要逆序遍历行;同理可得,顺序遍历列。以先行后列的遍历顺序为例。
- ⑤ 举例
dp[0][lens - 1]是最终的结果,同样给dp数组遍历赋值时,也只用到or赋值了右上三角形
代码
class Solution:
def longestPalindromeSubseq(self, s: str) -> int:
lens = len(s)
if lens <= 0:
return 0
# 初始化
dp = [[0] * lens for _ in range(lens)]
# 遍历,行逆序,列顺序,且j >= i
for i in range(lens - 1, -1, -1):
for j in range(i, lens):
if s[i] == s[j]:
if i == j: # 一个元素
dp[i][j] = 1
else:
dp[i][j] = dp[i + 1][j - 1] + 2
else:
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])
return dp[0][lens - 1] # 返回最右上角的值
学习收获:
回文子串
- 子串为连续子串
- 从回文串整体出发,s[i, j]首先判断边界的元素是否相等,再判断s[i + 1, j - 1]是否为回文串
- 因此定义二维dp数组,dp数组保存s[i, j]是否为回文串
- 递推式首先分类边界是否相等,相等再分类:i == j、j = i + 1、j >= i + 2。
最长回文子序列 - 子序列为非连续子序列
- 借鉴上一题的思路,定义二维dp保存最长回文子序列长度
- 递推式首先分类边界是否相等,相等再分类:i == j 、j = i + 1、j >= i + 2;不相等再分类:删除s[i]还是删除s[j]之后再求最长回文子序列
以及,上面两道题目明明递推式用到了dp[i +1][j - 1],但是没有围一圈,且初始化为全0,没有初始化dp[i + 1][j - 1]呢。 - 首先是dp数组定义要求 j >= i,因此最后一行只赋值了dp[lens - 1][lens - 1],赋值时没有用到dp[i + 1][j - 1]
- 后面遍历赋值dp数组时,用到dp[i + 1][j - 1]的情况时,这个dp是存在的。
- 而不是像之前最长公共子序列一样,如果不围一圈,且初始化没管dp[i - 1][j - 1]的话,然后将第0行归入遍历中。第0行遍历时会用到dp[i - 1][j - 1],但是这个dp是不存在的,从而出现问题,所以不围起来第0行和第0列有比较复杂的初始化。围起来的话,从dp定义出发,即使第0行和第0列要初始化,但是也没有那么麻烦