Leetcode刷题笔记------动态规划之子序列问题篇
一、回文
第一题:回文子串
Leetcode647. 回文子串:中等题 (详情点击链接见原题)
给你一个字符串
s
,请你统计并返回这个字符串中 回文子串 的数目。回文字符串 是正着读和倒过来读一样的字符串
1. 确定 dp
数组(dp table
)以及下标的含义
在定义 dp
数组的时候 很自然就会想题目求什么,我们就如何定义 dp
数组
布尔类型的dp[i][j]
:表示区间范围 [i,j]
的子串是否为回文子串,如果是则为True
2. 确定递推公式
当 s[i]
与 s[j]
不相等,dp[i][j]
一定是 false
当 s[i]
与 s[j]
相等时,有如下三种情况
case1:
下标 i
与下标 j
相同,同一个字符当然是回文子串
case2:
下标 i
与 j
相差为1
,如aa
的时候也是回文子串
case3:
下标i
与j
大于1的时候,例如cabac
,此时s[i] == s[j]
,判定区间[i,j]
是不是回文子串就看[i + 1, j - 1]
是不是回文(为True
)就可以了
3. dp
数组如何初始化
dp[i][j]
初始化为 false
,因为不可能一开始就全匹配上
4. 确定遍历顺序
这道题的遍历顺序有点讲究,如果按照我们的惯性思维从上到下,从左到右取遍历,那么就得不出结果
所以一定要从下到上,从左到右遍历,这样保证dp[i + 1][j - 1]
都是经过计算的
以cbabc
为例,对应的 dp
数组为
python代码解法
python
class Solution:
def countSubstrings(self, s: str) -> int:
dp = [[False] * len(s) for _ in range(len(s))]
result = 0 # result 用来保存回文子串的数目
for i in range(len(s) - 1, -1, -1): # 从下到上
for j in range(i, len(s)):
if s[i] == s[j]:
if j - i <= 1:
result += 1
dp[i][j] = True
elif dp[i + 1][j - 1]:
result += 1
dp[i][j] = True
return result
第二题:最长回文子串
Leetcode5:最长回文子串:中等题 (详情点击链接见原题)
给你一个字符串
s
,找到s
中最长的回文子串。如果字符串的反序与原始字符串相同,则该字符串称为回文字符串
python代码解法
python
class Solution:
def longestPalindrome(self, s: str) -> str:
dp = [[False] * len(s) for _ in range(len(s))]
max_len = 0 # result 用来保存回文子串的数目
result = ""
for i in range(len(s) - 1, -1, -1): # 从下到上
for j in range(i, len(s)):
if s[i] == s[j]:
if j - i <= 1 or dp[i + 1][j - 1]:
dp[i][j] = True
if dp[i][j] and j - i + 1 > max_len:
max_len = j - i + 1
result = s[i: i + max_len]
return result
二、子序列(连续)
第一题:最长重复子数组
Leetcode718. 最长重复子数组:中等题 (详情点击链接见原题)
给两个整数数组
nums1
和nums2
,返回 两个数组中 公共的 、长度最长的子数组的长度
解题思路:子数组其实就是连续子序列
1. 确定 dp
数组(dp table
)以及下标的含义
dp[i][j]
:以下标 i - 1
为结尾的 A
和以下标 j - 1
为结尾的 B
,最长重复子数组的长度为 dp[i][j]
【dp[i][j]
的定义决定了我们在遍历 dp[i][j]
的时候 i
和 j
都要从 1
开始】
以 A=[1, 2, 3, 2, 1],B = [3, 2, 1, 4, 7]为例,递推过程如下图所示:
dp[4][2] = 2
的含义为以 下标3
为结尾的 A
数组 与下标 1
为结尾的 B
数组 的最长重复子数组的长度为 2
2. 确定递推公式
dp[i][j]
的状态只能由 dp[i - 1][j - 1]
推导出来,即当 A[i - 1]
和 B[i - 1]
相等的时候,dp[i][j] = dp[i - 1][j - 1] + 1
3. dp数组如何初始化
根据 dp[i][j]
的定义,dp[i][0]
和 dp[0][j]
其实都是没有意义的【可以看成是以 i - 1
为结尾的 A
和空数组 B
的的最长重复子数组】, 故 dp[i][0]
和 dp[0][j]
初始化为 0
4. 确定遍历顺序
外层 for 循环遍历 A
, 内层 for
循环遍历 B
python代码解法
python
class Solution:
def findLength(self, nums1: List[int], nums2: List[int]) -> int:
n, m = len(nums1), len(nums2)
result = 0
dp = [[0 for _ in range(m + 1)] for _ in range(n + 1)]
for i in range(1, n + 1): # 外层循环遍历 nums1
for j in range(1, m + 1): # 内层循环遍历 nums2
if nums1[i - 1] == nums2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
result = max(result, dp[i][j])
# for i in dp: # 打印 dp 数组
# print(i)
return result
第二题:最长连续递增序列
Leetcode674. 最长连续递增序列:简单题 (详情点击链接见原题)
给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度
解题思路:
1. 确定 dp
数组(dp table
)以及下标的含义
dp[i]
: 以下标 i
为结尾的连续递增的子序列长度为 dp[i]
(注意这里说以下标 i
为结尾,并没说一定以下标 0
为起始位置)
2. 确定递推公式
如果 nums[i] > nums[i -1]
,那么以 i
为结尾的连续递增的子序列长度一定等于 以 i - 1
为结尾的连续递增的子序列长度 + 1
递推公式: dp[i] = dp[i - 1] + 1
本题要求的是连续递增子序列,所以只需要比较 nums[i]
与 nums[i - 1]
,而不用去比较 nums[j]
与 nums[i]
【j
在 0
到 i
之间遍历】
3. dp数组如何初始化
以下标 i
为结尾的连续递增的子序列长度最少也应该是 1
,即 nums[i]
这一个元素
4. 确定遍历顺序
从递推公式上可以看出,dp[i ]
依赖 dp[i - 1]
,所以一定是从前向后遍历
python代码解法(dp思路)
python
class Solution:
def findLengthOfLCIS(self, nums: List[int]) -> int:
dp = [1] * len(nums)
for i in range(1, len(nums)):
if nums[i] > nums[i - 1]:
dp[i] = dp[i - 1] + 1
return max(dp)
python代码解法(滑窗思路)
python
class Solution:
def findLengthOfLCIS(self, nums: List[int]) -> int:
left, right = 0, 1
ans = 1
while right < len(nums):
if nums[right] <= nums[right - 1]:
left = right
ans = max(ans, right - left + 1)
right += 1
return ans
第三题:最大子数组和
Leetcode53. 最大子数组和:中等题 (详情点击链接见原题)
给你一个整数数组
nums
,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和
1. 确定 dp
数组(dp table
)以及下标的含义
dp[i]
:包括下标 i
(以 nums[i]
为结尾)的最大连续子序列和为 dp[i]
2. 确定递推公式
dp[i]
只有两个方向可以推导出来:因为 dp[i - 1] < 0
的话会拉低连续子序列的和,如果拉低还不如直接从当前 nums[i]
开始算)
dp[i - 1] + nums[i]
,即加入 nums[i]
后的连续子序列和(
nums[i]
:从头开始计算当前连续子序列和
3. dp数组如何初始化
dp[0] = nums[0]
4. 确定遍历顺序
递推公式中 dp[i]
依赖于 dp[i - 1]
的状态,需要从前向后遍历
注:我们要找最大的连续子序列,就应该找每一个 i
为终点的连续最大子序列
python代码解法
python
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
dp = [0] * len(nums)
dp[0] = nums[0]
for i in range(1, len(nums)):
dp[i] = max(dp[i - 1] + nums[i], nums[i])
# print(dp)
return max(dp)
三、子序列(不连续)
第一题: 最长递增子序列
Leetcode300. 最长递增子序列:中等题 (详情点击链接见原题)
给你一个整数数组
nums
,找到其中最长严格递增子序列的长度子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序
解题思路:
相对于 Leetcode674. 最长连续递增序列 这一题,本题最大的区别在于不连续
1. 确定 dp
数组(dp table
)以及下标的含义
dp[i]
:表示 i
之前包括 i
的以 nums[i]
结尾的最长递增子序列的长度
2. 确定递推公式
位置 i
的最长升序子序列等于 j
从 0
到 i - 1
各个位置的最长升序子序列 + 1
的最大值
python
if nums[i] > nums[j]:
dp[i] = max(dp[i], dp[j + 1]) # 注意这里不是要 dp[i] 与 dp[j] + 1 进行比较, 而是取dp[j] + 1的最大值
3. dp
数组如何初始化
每一个i
,对应的 dp[i]
(即最长递增子序列 )起始大小至少都是 1
4. 确定遍历顺序
dp[i]
是有0
到i - 1
各个位置的最长递增子序列 推导而来,那么遍历 i
一定是从前向后遍历
j
其实就是遍历 0
到 i - 1
, 那么是从前到后还是从后到前都可以
python代码解法
python
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
dp = [1] * len(nums)
for i in range(1, len(nums)):
for j in range(0, i):
if nums[i] > nums[j]:
dp[i] = max(dp[i], dp[j] + 1)
# print(dp)
return max(dp)
第二题:最长公共子序列
Leetcode1143:最长公共子序列:中等题 (详情点击链接见原题)
给定两个字符串
text1
和text2
,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回0
1. 确定 dp
数组(dp table
)以及下标的含义
dp[i][j]
:长度为 [0, i - 1]
的字符串 text1
与长度为 [0, j - 1]
的字符串 text2
的最长公共子序列为 dp[i][j]
2. 确定递推公式
3. dp
数组如何初始化
dp[i][0]
:text1[0, i - 1]
和空串的最长公共子序列是0,dp[i][0] = 0
,同理 dp[0][j] = 0
4. 确定遍历顺序
从前向后,从上到下来遍历
python代码解法
python
class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
n, m = len(text1), len(text2)
dp = [[0 for _ in range(m + 1)] for _ in range(n + 1)]
for i in range(1, n + 1):
for j in range(1, m + 1):
if text1[i - 1] == text2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
else:
# text1[0, i - 2] 与 text2[0, j - 1]的最长公共子序列
# text1[0, i - 1] 与 text2[0, j - 2]的最长公共子序列
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
return dp[n][m]
第三题:不相交的线
Leetcode1035. 不相交的线:中等题 (详情点击链接见原题)
在两条独立的水平线上按给定的顺序写下
nums1
和nums2
中的整数
四、编辑距离
第一题:判断子序列
Leetcode392. 判断子序列:简单题 (详情点击链接见原题)
给定字符串
s
和t
,判断s
是否为t
的子序列。
解题思路
1. 确定 dp
数组(dp table
)以及下标的含义
dp[i][j]
:长度为 [0, i - 1]
的字符串 s
与长度为 [0, j - 1]
的字符串 t
的相同子序列的长度为 dp[i][j]
注:判断 s
是否为 t
的子序列。即 t
的长度是大于等于 s
的
2. 确定递推公式
python
if s[i - 1] == t[i - 1]: # t中找到一个字符在s中也出现了
dp[i][j] = dp[i - 1][j - 1] + 1
if s[i - 1] != t[i - 1]: # 相当于 t 要删除元素,继续匹配
dp[i][j] = dp[i][j - 1]
3. dp
数组如何初始化
dp[i][j]
是依赖于 dp[i - 1][j - 1]
的,所以 dp[0][0]
和 dp[i][0]
是一定要初始化的
4. 确定遍历顺序
dp[i][j]
都是依赖于 dp[i - 1][j - 1]
和 dp[i][j - 1]
,所以应该从前向后,从上到下来遍历
5. 举例推导dp数组
由于 dp[i][j]
表示以下标 i - 1
为结尾的字符串 s
和以下标 j - 1
为结尾的字符串 t
相同子序列的长度,所以如果 dp[len(s)][len(t)]
与字符串 s
的长度相同说明:s
与 t
的最长相同子序列就是 s
,那么 s
就是 t
的子序列
python代码解法
python
class Solution:
def isSubsequence(self, s: str, t: str) -> bool:
dp = [[0 for _ in range(len(t) + 1)] for _ in range(len(s) + 1)]
for i in range(1, len(s) + 1):
for j in range(1, len(t) + 1):
if s[i - 1] == t[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
else:
dp[i][j] = dp[i][j - 1]
return True if dp[len(s)][len(t)] == len(s) else False
第二题: 不同的子序列
Leetcode115. 不同的子序列:困难题 (详情点击链接见原题)
给你两个字符串
s
和t
,统计并返回在s
的 子序列 中t
出现的个数,结果需要对10的9次方 + 7
取模
解题思路:
本题相对于编辑距离还是比较简单的,因为本题只有删除操作,本题求的是 s
里面有多少个像 t
这样的子序列,其实就是问这个 s
字符串中有多少种删除元素的方式使得 s
可以变成 t
,
1. 确定 dp
数组(dp table
)以及下标的含义
dp[i][j]:
以 i - 1
为结尾的 s
子序列种出现以 j - 1
为结尾的 t
的 个数 为 dp[i][j]
2. 确定递推公式
case1: s[i - 1] 与 t[j - 1] 相等
s:bagg t: bag
s[3]
和 t[2]
是相同的,但是字符串 s
也可以不用 s[3]
来匹配,s[0]s[1]s[2]
和 s[0]s[1]s[3]
组成的 bag
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]
不需要考虑 s
和 t
的最后一位字母,只需要用 dp[i - 1][j - 1]
case2: s[i - 1]
与 t[j - 1]
不相等
当 s[i - 1]
与 t[j - 1]
不相等时,dp[i][j]
只有一部分组成,不用 s[i - 1]
来匹配
3. dp
数组如何初始化
由递推公式可知: dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]
; 和 dp[i][j] = dp[i - 1][j]
,dp[i][j] 是从上方和左上方推导而来的,所以 dp[i][0]
和 dp[0][j]
是一定要初始化的
dp[i][0]
: 以下标 i - 1
为结尾的 s
删除所有元素,出现空串t
【即一种删除所有元素的方式】
dp[0][j]
: 空串 s
无论怎么都变成不了 t
,所以 dp[0][j] = 0
dp[0][0] = 1
: 空字符串 s
可以删除 0
个元素变成空字符串 t
4. 确定遍历顺序
5. 举例推导dp数组
python代码解法
python
class Solution:
def numDistinct(self, s: str, t: str) -> int:
dp = [[0 for _ in range(len(t) + 1)] for _ in range(len(s) + 1)]
for i in range(len(s)):
dp[i][0] = 1
for j in range(1, len(t)):
dp[0][j] = 0
for i in range(1, len(s) + 1):
for j in range(1, len(t) + 1):
if s[i - 1] == t[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]
else:
dp[i][j] = dp[i - 1][j]
return dp[len(s)][len(t)]
第三题:两个字符串的删除操作
Leetcode583. 两个字符串的删除操作:中等题 (详情点击链接见原题)
给定两个单词
word1
和word2
,返回使得word1
和word2
相同所需的最小步数
解题思路
相对于上一题而言,其实就是两个字符串都可以删了
1. 确定 dp
数组(dp table
)以及下标的含义
dp[i][j]
: 以 i-1
为结尾的字符串 word1
,和以 j-1
位结尾的字符串 word2
,想要达到相等,所需要删除元素的最少次数
2. 确定递推公式
- 当
word1[i - 1]
与word2[j - 1]
相同的时候【不用删除元素】dp[i][j] = dp[i - 1][j - 1]
- 当
word1[i - 1]
与word2[j - 1]
不相同的时候:dp=min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + 2)
- case1:删
word1[i - 1]
,最少操作次数为dp[i - 1][j] + 1
- case2: 删
word2[j - 1]
,最少操作次数为dp[i][j - 1] + 1
- case3:同时删
word1[i - 1]
和word2[j - 1]
,最少操作次数为dp[i - 1][j - 1] + 2
- case1:删
3. dp
数组如何初始化
从递推公式中,可以看出来,dp[i][0]
和 dp[0][j]
是一定要初始化的
dp[i][0]
:word2
为空字符串,以 i-1
为结尾的字符串 word1
要删除多少个元素,才能和 word2
相同呢,很明显 dp[i][0] = i
, dp[0][j] = j
4. 确定遍历顺序
从递推公式 dp[i][j] = min(dp[i - 1][j - 1] + 2, min(dp[i - 1][j], dp[i][j - 1]) + 1)
; 和 dp[i][j] = dp[i - 1][j - 1]
可以看出 dp[i][j]
都是根据左上方、正上方、正左方推出来的
5.举例推导DP数组
以 word1:sea,word2:eat
为例
python代码解法
python
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
dp = [[0 for _ in range(len(word2) + 1)] for _ in range(len(word1) + 1)]
for i in range(len(word1) + 1):
dp[i][0] = i
for j in range(len(word2) + 1):
dp[0][j] = j
for i in range(1, len(word1) + 1):
for j in range(1, len(word2) + 1):
if word1[i - 1] == word2[j - 1]:
dp[i][j] = dp[i - 1][j - 1]
else:
dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1)
return dp[len(word1)][len(word2)]
第四题:编辑距离
Leetcode72. 编辑距离:中等题 (详情点击链接见原题)
给你两个单词
word1
和word2
, 请返回将word1
转换成word2
所使用的最少操作数`
1. 确定 dp
数组(dp table
)以及下标的含义
dp[i][j]
:表示以下标 i - 1
为结尾的字符串 word1
和以下标 j - 1
为结尾的字符串 word2
,最近的编辑距离为 dp[i][j]
2. 确定递推公式
if word1[i - 1] != word2[j - 1]
操作1 : word1
删除一个元素,那么就是以下标 i - 2
为结尾的 word1
与 j-1
为结尾的 word2
的最近编辑距离 再加上一个操作dp[i][j] = dp[i - 1][j] + 1
操作2 :word2
删除一个元素,那么就是以下标 i - 1
为结尾的 word1
与 j-2
为结尾的 word2
的最近编辑距离 再加上一个操作,dp[i][j] = dp[i][j - 1] + 1
添加元素怎么操作呢?word2
添加一个元素,相当于 word1
删除一个元素
比如 word1 = 'ad', word2 = 'a'
,word1
删除元素 d
,word1 = 'a', word2 = 'a'
【操作一次】
和 word2
添加一个元素 d
,word1 = 'ad', word2 = 'ad'
【操作一次】
操作3 :替换元素
word1
替换 word1[i - 1]
使其与 word2[j - 1]
相同,此时不用增删元素,只需一次替换操作就可以让 word1[i - 1]
和 word2[j - 1]
, dp[i][j] = dp[i - 1][j - 1] + 1
python
if word1[i - 1] == word2[j - 1]: # 既然两个元素相同
dp[i][j] = dp[i - 1][j - 1] # 考虑以 i - 2 为下标结尾的 word1 和以 j - 2为下标结尾的 word2 的最近的编辑距离
if word1[i - 1] != word2[j - 1]: # 有增,删,和替换三种操作]
dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1
3. dp
数组如何初始化
dp[i][0]
:以下标 i-1
为结尾的字符串 word1
,和空字符串 word2
,最近编辑距离为 dp[i][0]
,dp[i][0] = i
即对 word1 里面的元素全部都做删除操作
4. 确定遍历顺序
dp[i][j] = dp[i - 1][j - 1]
dp[i][j] = dp[i - 1][j - 1] + 1
dp[i][j] = dp[i][j - 1] + 1
dp[i][j] = dp[i - 1][j] + 1
可以看出dp[i][j]
是依赖左方,上方和左上方元素的,所以dp
矩阵中一定是从左到右从上到下去遍历
python代码解法
python
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
dp = [[0 for _ in range(len(word2) + 1)] for _ in range(len(word1) + 1)]
for i in range(0, len(word1) + 1):
dp[i][0] = i
for j in range(0, len(word2) + 1):
dp[0][j] = j
for i in range(1, len(word1) + 1):
for j in range(1, len(word2) + 1):
if word1[i - 1] == word2[j - 1]:
dp[i][j] = dp[i - 1][j - 1]
else:
dp[i][j] = min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]) + 1
return dp[len(word1)][len(word2)]