# 代码随想录算法训练营Day37 | Leetcode300.最长递增子序列、674.最长连续递增序列、718.最长重复子数组
代码随想录算法训练营Day37 | Leetcode300.最长递增子序列、674.最长连续递增序列、718.最长重复子数组
一、最长递增子序列
相关题目:Leetcode300
文档讲解:Leetcode300
视频讲解:Leetcode300
1. Leetcode300.最长递增子序列
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
提示:
- 1 <= nums.length <= 2500
- -104 <= nums[i] <= 104
-
思路:
- 动规五部曲:
- 确定 dp 数组以及下标的含义:dp[i] 表示 i 前包括 i 的以 nums[i] 结尾的最长递增子序列的长度。定义以 nums[i] 结尾是因为做递增比较的时候,如果比较 nums[j] 和 nums[i] 的大小,那么两个递增子序列一定分别以 nums[j] 和 nums[i] 为结尾, 不是尾部元素的比较难以算递增。
- 状态转移方程:位置 i 的最长升序子序列等于 j 从 0 到 i-1 各个位置的最长升序子序列 + 1 的最大值。所以:if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1)。
- dp[i] 的初始化:对每一个 i,对应的 dp[i](即最长递增子序列)起始大小至少都是 1。
- 确定遍历顺序:dp[i] 是由 0 到 i-1 各个位置的最长递增子序列 推导而来,那么遍历 i 一定是从前向后遍历。j 只要把 0 到 i-1 的元素都遍历了就行,无论是从前到后还是从后到前遍历都可以(默认从前向后遍历)。
- 举例推导 dp 数组:输入:[0,1,0,3,2],dp 数组的变化如下:
- 动规五部曲:
-
动规
class Solution:def lengthOfLIS(self, nums: List[int]) -> int:if len(nums) <= 1:return len(nums)dp = [1] * len(nums)result = 1for 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)result = max(result, dp[i]) #取长的子序列return result
二、最长连续递增序列
相关题目:Leetcode674
文档讲解:Leetcode674
视频讲解:Leetcode674
1. Leetcode674.最长连续递增序列
给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。
连续递增的子序列 可以由两个下标 l 和 r(l < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], …, nums[r - 1], nums[r]] 就是连续递增子序列。
提示:
- 1 <= nums.length <= 104
- -109 <= nums[i] <= 109
- 思路:
- 动规五部曲:
- 确定 dp 数组以及下标的含义:dp[i] 表示以下标 i 为结尾的连续递增的子序列长度为 dp[i]。(以下标 i 为结尾,并不是说一定以下标 0 为起始位置)
- 确定递推公式:如果 nums[i] > nums[i - 1],那么以 i 为结尾的连续递增的子序列长度 一定等于以 i - 1 为结尾的连续递增的子序列长度 + 1 ,所以 dp[i] = dp[i - 1] + 1。
- dp 数组如何初始化:以下标 i 为结尾的连续递增的子序列长度最少也应该是 1,即 nums[i] 这一个元素。所以 dp[i] 皆应初始为 1。
- 确定遍历顺序:从递推公式可以看出 dp[i + 1] 依赖 dp[i],所以一定是从前向后遍历。
- 举例推导 dp 数组:以输入 nums = [1,3,5,4,7] 为例,dp 数组状态如下:
- 动规五部曲:
- 注意:
- 与 Leetcode300.最长递增子序列 不同,本题要求连续递增子序列,所以就只要比较 nums[i] 与 nums[i - 1],而不需比较 nums[j] 与 nums[i] (j 在 0 到 i 之间遍历)。
- 与 Leetcode300.最长递增子序列 不同,本题要求连续递增子序列,所以就只要比较 nums[i] 与 nums[i - 1],而不需比较 nums[j] 与 nums[i] (j 在 0 到 i 之间遍历)。
- 动规
###DP
class Solution:def findLengthOfLCIS(self, nums: List[int]) -> int:if len(nums) == 0:return 0result = 1dp = [1] * len(nums)for i in range(len(nums)-1):if nums[i+1] > nums[i]: #连续记录dp[i+1] = dp[i] + 1result = max(result, dp[i+1])return result###DP(优化版)
class Solution:def findLengthOfLCIS(self, nums: List[int]) -> int:if not nums:return 0max_length = 1current_length = 1for i in range(1, len(nums)):if nums[i] > nums[i - 1]:current_length += 1max_length = max(max_length, current_length)else:current_length = 1return max_length
- 贪心
class Solution:def findLengthOfLCIS(self, nums: List[int]) -> int:if len(nums) == 0:return 0result = 1 #连续子序列最少也是1count = 1for i in range(len(nums)-1):if nums[i+1] > nums[i]: #连续记录count += 1else: #不连续,count从头开始count = 1result = max(result, count)return result
三、最长重复子数组
相关题目:Leetcode718
文档讲解:Leetcode718
视频讲解:Leetcode718
1. Leetcode718.最长重复子数组
给两个整数数组 nums1 和 nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度 。
提示:
- 1 <= nums1.length, nums2.length <= 1000
- 0 <= nums1[i], nums2[i] <= 100
- 思路:
- 题目中说的子数组其实就是连续子序列。要求两个数组中最长重复子数组,如果是暴力的解法需要先两层 for 循环确定两个数组起始位置,然后再来一个循环可以是 for 或者 while,来从两个起始位置开始比较,取得重复子数组的长度。如果利用动态规划,利用二维数组可以记录两个字符串的所有比较情况。
- 动规五部曲:
- 确定 dp 数组以及下标的含义:dp[i][j] 表示以下标 i - 1 为结尾的 A,和以下标 j - 1 为结尾的 B,最长重复子数组长度为 dp[i][j]。
- 确定递推公式:根据 dp[i][j] 的定义,dp[i][j] 的状态只能由 dp[i - 1][j - 1] 推导出来。即当 A[i - 1] 和 B[j - 1] 相等的时候,dp[i][j] = dp[i - 1][j - 1] + 1。
- dp 数组如何初始化:根据 dp[i][j] 的定义,dp[i][0] 和 dp[0][j] 没有意义,但 dp[i][0] 和 dp[0][j] 要初始值,为了方便递归公式 dp[i][j] = dp[i - 1][j - 1] + 1,所以 dp[i][0] 和 dp[0][j] 初始化为 0。例:如果 A[0] 和 B[0] 相同的话,dp[1][1] = dp[0][0] + 1,只有 dp[0][0] 初始为 0,正好符合递推公式逐步累加起来。
- 确定遍历顺序:外层 for 循环遍历 A,内层 for 循环遍历 B 或是 外层 for 循环遍历 B,内层 for 循环遍历 A 都可以。
- 举例推导 dp 数组:以 A: [1,2,3,2,1],B: [3,2,1,4,7] 为例,dp 数组的状态变化如下:
- 注意:
- 前述 dp[i][j] 的定义决定着在遍历 dp[i][j] 的时候 i 和 j 都要从1开始。
- 定义 dp[i][j] 为以下标 i 为结尾的 A,和以下标 j 为结尾的 B,最长重复子数组长度也是可行的,但需要单独处理初始化部分:
- 第一行和第一列需要进行初始化,如果 nums1[i] 与 nums2[0] 相同的话,对应的 dp[i][0] 要初始为 1, 因为此时最长重复子数组为 1;nums2[j] 与 nums1[0] 相同的话,同理。
- 利用滚动数组(一维 dp):可以看出 dp[i][j] 都是由 dp[i - 1][j - 1] 推出,那么压缩可以一维数组,也就是 dp[j] 都是由 dp[j - 1] 推出。相当于可以把上一层 dp[i - 1][j] 拷贝到下一层 dp[i][j] 来继续用。此时遍历 B 数组的时候,就要从后向前遍历,避免重复覆盖。例(同上):
- 二维 dp
###2维DP
class Solution:def findLength(self, nums1: List[int], nums2: List[int]) -> int:# 创建一个二维数组 dp,用于存储最长公共子数组的长度dp = [[0] * (len(nums2) + 1) for _ in range(len(nums1) + 1)]# 记录最长公共子数组的长度result = 0# 遍历数组 nums1for i in range(1, len(nums1) + 1):# 遍历数组 nums2for j in range(1, len(nums2) + 1):# 如果 nums1[i-1] 和 nums2[j-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###2维DP 扩展
class Solution:def findLength(self, nums1: List[int], nums2: List[int]) -> int:# 创建一个二维数组 dp,用于存储最长公共子数组的长度dp = [[0] * (len(nums2) + 1) for _ in range(len(nums1) + 1)]# 记录最长公共子数组的长度result = 0# 对第一行和第一列进行初始化for i in range(len(nums1)):if nums1[i] == nums2[0]:dp[i + 1][1] = 1for j in range(len(nums2)):if nums1[0] == nums2[j]:dp[1][j + 1] = 1# 填充dp数组for i in range(1, len(nums1) + 1):for j in range(1, len(nums2) + 1):if nums1[i - 1] == nums2[j - 1]:# 如果 nums1[i-1] 和 nums2[j-1] 相等,则当前位置的最长公共子数组长度为左上角位置的值加一dp[i][j] = dp[i - 1][j - 1] + 1if dp[i][j] > result:# 更新最长公共子数组的长度result = dp[i][j]# 返回最长公共子数组的长度return result
- 一维 dp
class Solution:def findLength(self, nums1: List[int], nums2: List[int]) -> int:# 创建一个一维数组 dp,用于存储最长公共子数组的长度dp = [0] * (len(nums2) + 1)# 记录最长公共子数组的长度result = 0# 遍历数组 nums1for i in range(1, len(nums1) + 1):# 用于保存上一个位置的值prev = 0# 遍历数组 nums2for j in range(1, len(nums2) + 1):# 保存当前位置的值,因为会在后面被更新current = dp[j]# 如果 nums1[i-1] 和 nums2[j-1] 相等if nums1[i - 1] == nums2[j - 1]:# 在当前位置上的最长公共子数组长度为上一个位置的长度加一dp[j] = prev + 1# 更新最长公共子数组的长度if dp[j] > result:result = dp[j]else:# 如果不相等,将当前位置的值置为零dp[j] = 0# 更新 prev 变量为当前位置的值,供下一次迭代使用prev = current# 返回最长公共子数组的长度return result