LeetCode 1143.最长公共子序列:
思路:
首先分析题目,最长公共子序列,子序列中相邻的元素在数组中可以不相邻但是前后位置要相同,与最长重复子数组有相似之处,区别在于子序列中的元素在原数组中可以不相邻,即“不连续”
动规五部曲:
- dp数组及含义:
dp[i][j]:是text1[0,i-1]和text2[0,j-1]的最长公共子序列,且没有要求子序列结尾为text1[i-1]或text2[j-1],原因会在递推式中解释,dp[i][j]还是对应的text1[i-1]和text2[j-1],和之前的原因一样,为了便于初始化,以及能将第1行和第1列的处理并入遍历的处理中 - 递推式:
这里分析递推式时我们先抛开之前那道题目的影响。从text1[i-1]与text2[j-1]是否相等来分类:- text1[i-1] = text2[j-1],那么如下图,只要在text1[0, i - 2]和text2[0, j- 2]的最长公共子序列 + 1即可,即dp[i][j] = dp[i-1][j-1] + 1
- text1[i-1] != text2[j - 1],那么text1[0, i-1]和text2[0, j-1]的最长公共子序列就应当在下面两张图中取最大值(有点像递归)
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
相应的,如果是最长公共子数组的话,如果按照子序列的递推公式的话,在text1[i - 1] = text2[j - 1]时,推导会有问题,因为不知道dp[i - 1][j - 1]得到的最长公共子数组的长度是不是text1[i - 2] = text2[j - 1],从而不能保证子数组是连续子数组,因此最长公共子数组的dp数组需要保证dp[i][j]的结尾为text1[i - 1]和text2[j - 1]的最长公共子数组的长度,而子序列不需要。
(可能也是因为递推时比较的是text1[i - 1]和text2[j - 1],而不是像求最长递增非连续子数组长度一样(这个要比较新元素和结尾元素),所以这个dp不需要包含text1[i-1]或text2[j - 1],而最长递增非连续子数组需要包含nums[i]?)(可能是要求连续的dp数组都要包含nums[i],非连续看情况)
- text1[i-1] = text2[j-1],那么如下图,只要在text1[0, i - 2]和text2[0, j- 2]的最长公共子序列 + 1即可,即dp[i][j] = dp[i-1][j-1] + 1
- 初始化
初始化为全0 - 遍历顺序:
由递推式可知,dp[i][j]由下图中三个蓝色部分推导得到,所以只要是在遍历到dp[i][j]时,三个蓝色部分已经被赋值就可以了。先行后列/先列后行均可,但是要顺序遍历
- 举例
因为dp数组定义没有要求公共子数组结尾一定要为text1[i-1]或text2[j-1],因此dp[-1][-1]的值就是最长公共子数组的值
class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
len1, len2 = len(text1), len(text2)
# 初始化
dp = [[0] * (len2 + 1) for _ in range(len1 + 1)]
# 遍历
for i in range(1, len1 + 1):
for j in range(1, len2 + 1):
if text1[i - 1] == text2[j - 1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i][j-1], dp[i-1][j])
return dp[len1][len2]
压缩二维数组为一维数组
以按行压缩为例,因为dp由三个部分推导得到,压缩为一维数组的话,不管遍历 j 是顺序还是逆序,都会因为dp[i-1][j-1]导致三个部分缺少一个部分,因此直接采取pre保存dp[i-1][j-1],而dp[i][j]需要dp[i][j-1],因此对 j 采取顺序遍历。
递推公式为:
if text1[i-1] == text2[j-1]:
dp[j] = pre + 1
else:
dp[j] = max(dp[j], dp[j-1])
需要注意的是:
- 在赋值dp[j]之前,需要额外保存其原来的数据作为dp[j+1]的pre
- 遍历时,每一行开始遍历前要将pre重置为0,不然pre就是上一行结尾的数,那么后面的推导就会出错
'''
按行压缩
'''
class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
len1, len2 = len(text1), len(text2)
# 初始化
dp = [0] * (len2 + 1)
for i in range(1, len1 + 1):
pre = 0 # 存储dp[i-1][j-1],同时要注意pre在每一行遍历开始前要置为0,即dp[i][0] = 0
print('i: ' + str(i))
for j in range(1, len2 + 1):
cur = dp[j] # 存储dp[i-1][j],也就是下一轮循环的pre
if text1[i - 1] == text2[j - 1]:
dp[j] = pre + 1
else:
dp[j] = max(dp[j], dp[j-1])
pre = cur
return dp[len2]
LeetCode 1035.不相交的线:
思路:
仔细分析这道题可知,
- 连线的要求为nums1[i] = nums2[j],数组的公共值
- 而线不相交表明连线的相对位置要和其数组的值在数组中的相对位置相同,
- 多个连线不能有同一个端点,确保是求两个数组的公共子序列
- 最后,由示例可知,公共子序列中的数在原数组中可以不相邻。
最后得到结论,这道题与上一题一摸一样,只是表现形式不同,实际思路和代码基本相同
LeetCode 53.最大子序和:
思路
首先分析题目,要求的是连续最大子数组和,且子数组最少包含一个元素。
动规五部曲:
- dp数组及含义:
dp[i] : nums[0, i]**包含nums[i]**的连续最大子数组和。 - 递推公式:
因为是连续的,因此遍历到nums[i]时只需要判断,nums[i]是要加入nums[i - 1]的子数组中,还是自己成为新的子数组。因为求最大和,那么当然是哪个最大拿哪个,那么就是dp[i - 1] >= ? 0(因为新子数组一定包含nums[i])
if dp[i - 1] >= 0:
dp[i] = dp[i - 1] + nums[i]
else:
dp[i] = nums[i]
- 初始化
根据递推公式可知,需要初始化dp[0],根据dp数组的含义可知,dp[0] = nums[0]。 - 举例
最后的结果在dp数组中求最大值
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
len_nums = len(nums)
if len_nums <= 0:
return 0
# 初始化
dp = [0] * len_nums
dp[0] = nums[0]
result = dp[0] # 遍历过程中求最大值
# 遍历
for i in range(1, len_nums):
if dp[i - 1] >= 0:
dp[i] = dp[i - 1] + nums[i] # 连接前面的子数组
else:
dp[i] = nums[i] # 重新开辟一个部分,自己是子数组的第一个元素
if dp[i] > result:
result = dp[i]
return result
还有一种贪心的方法在前面提到过
LeetCode 392.判断子序列:
思路:
分析题目,要求判断s是否为 t 的子序列,且子序列不是连续子序列。
动规五部曲:
- dp数组及含义:
可以联系到最长公共子数组or子序列的题目,二维dp存储所有情况。
dp[i][j] :s[0:i - 1]是否为t[0:j-1]的子序列,且子序列结尾一定是s[i - 1] - 递推式:
根据s[i - 1]是否等于t[j - 1]来判断- s[i - 1] == t[j - 1]:dp[i][j] = dp[i - 1][j - 1]
- s[i - 1] != t[j - 1]:因为是要判断s是否为 t 的子序列,所以只能删除 t 的结尾元素,不删除s的结尾元素,再进行判断,dp[i][j] = dp[i][j - 1]
- 初始化
根据dp数组定义,因为dp是是否,所以可以以0/1 和False/True来使用,以0/1为例,dp数组中的元素默认初始化为0,由于空序列也是子序列,因此初始化dp[0][j]为1(首先看到递推式都是等于,所以初始化一定要有1,→想到dp[0][0]→想到空序列是子序列→初始化第0行为全1) - 遍历顺序
二维dp数组,由递推公式有,先行后列/先列后行均可,但是顺序遍历(先行后列),先列后行顺序逆序均可,因为只要到dp[i][j]时dp[i-1][j-1]和dp[i][j-1]被赋值即可 - 举例
因为dp限定子序列结尾为s[i - 1]与题目相符:判断 s 是否为 t 的子序列,也是需要限定子序列结尾为s[i - 1],因此dp[-1][-1]的结果即为是/否(dp为0/1的话返回加判断, False/True的话直接返回)
代码:
class Solution:
def isSubsequence(self, s: str, t: str) -> bool:
lens, lent = len(s), len(t)
if lens <= 0: # 空字符也是子序列
return True
if lent <= 0: # s非空, t为空,s一定不是t的子序列
return False
# 初始化
dp = [[0] * (lent + 1) for _ in range(lens + 1)]
for j in range(lent + 1):
dp[0][j] = 1
# 遍历
for i in range(1, lens + 1):
for j in range(1, lent + 1):
if s[i - 1] == t[j - 1]:
dp[i][j] = dp[i - 1][j - 1]
else:
dp[i][j] = dp[i][j - 1]
return dp[lens][lent] == 1
根据递推公式,按列压缩二维数组为一维
- dp[i][j]依赖于下图中的蓝色部分,如果按行压缩的话,需要pre额外保存dp[i-1][j - 1](dp[i - 1][j - 1]和dp[i][j-1]压缩成一个了,但要用到两个,所以要额外保存)。
- 如果按列压缩的话,dp[i-1][j-1](对应dp[i-1])就不需要额外保存,但是因为要用到dp[i-1][j-1],所以遍历这一列时应当逆序遍历(不然dp[i-1]的值会被覆盖为这一列的,结果就不对了)。因此,遍历顺序为先列后行,遍历行为逆序遍历。
- 递推式为dp[i] = dp[i - 1] if s[i - 1] == t[j - 1] else dp[i]
代码:
"""
按列压缩二维dp为一维dp数组
"""
class Solution:
def isSubsequence(self, s: str, t: str) -> bool:
lens, lent = len(s), len(t)
if lens <= 0:
return True
if lent <= 0:
return False
# 初始化
dp = [0] * (lens + 1)
dp[0] = 1 # 初始化为True
# 遍历,先列后行,因为是按列压缩
for j in range(1, lent + 1):
for i in range(lens, 0, -1): # 逆序遍历
if s[i - 1] == t[j - 1]:
dp[i] = dp[i - 1] # 即dp[i][j] = dp[i-1][j-1]
return dp[lens] == 1
dp数组也可以定义为s[0,i-1]和t[0,j-1]的结尾为s[i-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]
代码
"""
dp存储相同子序列的长度,且限定结尾为s[i - 1]
"""
class Solution:
def isSubsequence(self, s: str, t: str) -> bool:
lens, lent = len(s), len(t)
if lens <= 0:
return True
if lent <= 0:
return False
# 初始化
dp = [[0] * (lent + 1) for _ in range(lens + 1)]
# 遍历
for i in range(1, lens + 1):
for j in range(1, lent + 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 dp[lens][lent] == lens
学习收获:
最长公共子序列:
- dp数组不要求子序列结尾为两个数组的值。(要求子序列连续的时候,结尾要限定;不连续的时候,看情况结尾是否要限定)(类比题目:最长递增子序列,最长连续递增序列,最长重复子数组)
- 递推式根据text1[i-1] =? text2[j - 1]来判断,不相等的时候推递推式有点像递归
不相交的线: - 最长公共子序列的题目描述包装了一下,思路基本相同
最大子序和: - 题目要求连续,因此dp要限定nums[i]为结尾
- 推导递推式就是判断nums[i]是否要加入nums[i - 1]的子序列中
判断子序列:s 是否为 t 的子序列 - 编辑距离问题的入门题目
- 二维dp保存所有结果,且dp限定结尾为s[i - 1]
- 递推式,dp[i][j]只由dp[i - 1][j - 1]和dp[i][j - 1]来,当结尾不相同时,只删 t 的结尾,不删 s 的结尾(因为要判断s 是否为子序列啊)
- 最后结果,因为判断s是否为子序列,所以,虽然dp限定了子序列的结尾,但是最后结果是dp[-1][-1]