LeetCode 300.最长递增子序列:
思路:
需要分析题目看懂题目要求的子序列的定义
还是使用动态规划,动规五部曲
- dp数组及含义:
dp[i]表示[0, i]结尾元素为nums[i]的最长递增子序列长度(要规定结尾元素为nums[i]以便递推时比较新元素是加入序列还是新序列)
需要注意的是,dp数组这样规定后,那么nums数组的最长递增子序列的长度不一定在dp[-1],而是需要dp数组中求最大值 - 递推式:
- 最开始想到的是dp[i]与dp[i - 1]做递推,但是如果nums[i] <= nums[i - 1],dp[i]就不知道怎么赋值了,如果dp[i]赋值为1,也不对,因为前面[0, i-2]可能有能让nums[i]加入的子序列。
- 因此想到dp[i]与dp[j]做递推,j ∈[0, i - 1],也就是在 j 的范围内找结尾元素的值 < nums[i]且长度最长的子序列,然后将nums[i]加入该子序列。
if nums[i] > nums[j]:
dp[i] = max(dp[i], dp[j] + 1)
- 初始化:
由于dp数组的定义,所以dp全部初始化为1 - 遍历顺序
由递推公式有,遍历顺序应当是从前往后 - 举例
最后结果是dp数组求最大值
代码
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
len_nums = len(nums)
if len_nums <= 1: # 这里要区分一下
if len_nums == 0:
return 0
return 1
# 初始化
dp = [1] * len_nums
result = 1 # 记录最大子序列长度
# 遍历
for i in range(1, len_nums):
for j in range(i): # 在[0, i-1]的子序列中找结尾元素 < nums[i]且长度最长的
if nums[i] > nums[j]: dp[i] = max(dp[i], dp[j] + 1)
if dp[i] > result: # 最大长度要在dp数组中再求max
result = dp[i]
return result
LeetCode 674.最长连续递增序列:
思路:
首先分析题目,要求得到的是最长连续递增子序列,其中的元素是严格递增,且子序列中的元素在nums数组中是相邻的。
与上面题目相似,但是由于要求子序列元素在nums数组中是相邻的,所以递推式只需要比较nums[i]和nums[i - 1]即可。(或者说,如果符合条件的话,nums[i]只能接到nums[i - 1]的序列之后)
动规五部曲:
- dp数组及含义:
dp[i]:[0, i]结尾元素为nums[i]的最长连续递增子序列的长度.
同时也是,要求的最长连续递增子序列的长度是dp数组求max,不一定是dp[-1]的值 - 递推式
dp[i]由dp[i - 1]推导来
if nums[i] > nums[i - 1]:
dp[i] = dp[i - 1] + 1
- 初始化:
dp数组全部初始化为1 - 遍历顺序
由递推公式有,遍历顺序为从前往后 - 举例
代码:
class Solution:
def findLengthOfLCIS(self, nums: List[int]) -> int:
len_nums = len(nums)
if len_nums <= 1:
if len_nums == 0:
return 0
return 1
# 初始化
dp = [1] * len_nums
result = 1 # 保存最大值
# 遍历
for i in range(1, len_nums):
if nums[i] > nums[i - 1]:
dp[i] = dp[i - 1] + 1
if dp[i] > result:
result = dp[i]
return result
LeetCode 718.最长重复子数组:
思路:
首先分析题目:题目中实际要求的是得到公共连续最长子数组,只要想到动态规划二维dp数组保存两个数组匹配的所有情况。
动规五部曲:
- dp数组及含义:
dp[i][j]:A[0, i - 1]与B[0, j - 1]的结尾为A[i-1]和B[j-1]的最长公共连续子数组的长度。
dp[i][j]对应A[i-1]和B[j-1]会在后面说明 - 递推
由dp含义可知,dp[i][j]由dp[i - 1][j - 1]推导而来(或者说就是判断A[i-1]和B[j-1]是否相等)
if A[i-1] == B[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
- 初始化
初始化为全0。
在此解释为什么dp[i][j]对应A[i-1]和B[j-1],如果dp[i][j]对应A[i]和B[j],那么初始化时,dp[0][0]需要单独判断,且递推公式dp[i][j]需要由dp[i-1][j-1]推导得到,那么还需要判断第0行和第0列的数值。也就是需要单独判断第0行和第0列,但是如果在外面加一圈的话,只需要初始化为全0,第1行和第1列被包括在循环中。 - 遍历顺序
先行后列 or 先列后行均可,但是都要从前往后遍历,因为递推要用到dp[i-1][j-1] - 举例
最终结果需要在dp数组中求max
class Solution:
def findLength(self, nums1: List[int], nums2: List[int]) -> int:
len_n1, len_n2 = len(nums1), len(nums2)
# 初始化
dp = [[0] * (len_n2 + 1)for _ in range(len_n1 + 1)] # 外面包了一圈
result = 0
# 遍历
for i in range(1, len_n1 + 1): # 这里 i, j 范围为[1, len]
for j in range(1, len_n2 + 1):
if nums1[i - 1] == nums2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
if dp[i][j] > result:
result = dp[i][j]
return result
压缩二维dp数组为一维
- 可以按行压缩,或者按列压缩
- 以按行压缩为例,有需要注意的地方
- dp[j]依赖于上一行的dp[j-1],如果顺序遍历 j ,那么dp[j-1]就被覆盖为这一行的dp[j-1]了,所以要逆序遍历
- 递推公式也有区别,原来二维dp,当A[i-1] != B[j-1]时不需要操作,但是一维需要设置dp[j] = 0,不然的话dp[j]会继承上一行的dp[j],那么结果就不对了
'''
按行压缩二维dp数组需要注意的地方:
1. j需要逆序遍历,二维dp[i][j]要用到dp[i-1][j-1],对应一维就是dp[j]用到上一行的dp[j-1],
如果顺序遍历的话,dp[j-1]就变成这一行的,覆盖了上一行的dp[j-1],所以要逆序
2. 递推公式改变,二维是if nums1[i-1] == nums2[j-1]:dp[i][j] = dp[i-1][j-1],
但是一维应当是if 不变,else dp[j] = 0,不然的话明明结尾元素不相同,长度应当为0,但是dp[j]继承了上一行的数值,可能不是0了
'''
class Solution:
def findLength(self, nums1: List[int], nums2: List[int]) -> int:
len_n1, len_n2 = len(nums1), len(nums2)
# 初始化,按行压缩为一维数组
dp = [0] * (len_n2 + 1)
result = 0
# 遍历
for i in range(1, len_n1 + 1):
for j in range(len_n2, 0, -1): # j要逆序遍历,以免覆盖dp[i-1][j-1]的数值
if nums1[i - 1] == nums2[j - 1]:
dp[j] = dp[j - 1] + 1
else:
dp[j] = 0
if dp[j] > result:
result = dp[j]
return result
学习收获:
首先是最长递增子序列和最长连续递增子序列:
- 区别在于子序列中相邻的元素在原数组中是否相邻,从而递推公式中是只能在nums[i-1]后添加元素nums[i]还是num[0: i ]中找元素值小且最长的子序列添加元素nums[i]
- 同时还要注意是否严格递增,就是相邻的元素,后面的值是否一定大于前面的值,不能相等
- 最后dp数组的含义是,以nums[i]为结尾的子序列的最大长度(还有其它条件可以加上)
然后是两个数组的最长公共连续子数组:
- 要想到二维dp数组保存匹配的所以结果,同时因为是连续的,所以dp[i][j]只依赖于dp[i-1][j-1],
- 以及为了便于初始化,dp的大小为(len1 + 1)*(len2 + 1),dp[i][j]对应A[i-1]和B[j-1]
- 同时按行 / 按列压缩为一维时,需要注意列逆序 / 行逆序,同时元素值不相等时dp要赋值为0