Leetcode刷题笔记52:动态规划13(300.最长递增子序列-674. 最长连续递增序列-718. 最长重复子数组)

115 阅读3分钟

导语

leetcode刷题笔记记录,主要记录题目包括:

Leetcode 300. 最长递增子序列

题目描述

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

 

示例 1:

输入: nums = [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长递增子序列是 [2,3,7,101],因此长度为 4 。

示例 2:

输入: nums = [0,1,0,3,2,3]
输出: 4

示例 3:

输入: nums = [7,7,7,7,7,7,7]
输出: 1

 

提示:

  • 1 <= nums.length <= 2500
  • -104 <= nums[i] <= 104

 

进阶:

  • 你能将算法的时间复杂度降低到 O(n log(n)) 吗?

解法

于每一个新元素nums[i],我们遍历其之前所有的元素,看是否能找到一个递增的子序列,使得我们可以把nums[i]添加到该子序列的末尾。使用动规五部曲:

  1. dp[i]表示以nums[i]结尾的最长递增子序列(LIS)的长度。
  2. 递推公式:循环遍历所有i之前的j,若nums[i]>nums[j],则取dp[i]=max(dp[j]+1,dp[i])dp[i]=max(dp[j]+1,dp[i]);
  3. 初始化:dp[i]=1
  4. 遍历顺序:外层循环从前向后,内层循环无所谓
  5. 打印dp数组:略
class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        # 初始化 dp 数组,所有位置都设为 1,因为单个元素自身就是一个长度为 1 的递增子序列
        dp = [1] * len(nums)
        
        # 从第二个元素开始遍历整个数组
        for i in range(1, len(nums)):
            # 对于每一个元素,查找在其之前的所有元素
            for j in range(i):
                # 如果当前元素(nums[i])大于其之前的某个元素(nums[j])
                if nums[i] > nums[j]:
                    # 更新 dp[i],这里是寻找以 nums[i] 结尾的最长递增子序列
                    dp[i] = max(dp[j] + 1, dp[i])
                    
        # 返回最长递增子序列的长度,即 dp 数组中的最大值
        return max(dp)

Leetcode 674. 最长连续递增序列

题目描述

给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。

连续递增的子序列 可以由两个下标 l 和 rl < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]] 就是连续递增子序列。

 

示例 1:

输入: nums = [1,3,5,4,7]
输出: 3
解释: 最长连续递增序列是 [1,3,5], 长度为3。
尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 5 和 7 在原数组里被 4 隔开。 

示例 2:

输入: nums = [2,2,2,2,2]
输出: 1
解释: 最长连续递增序列是 [2], 长度为1。

 

提示:

  • 1 <= nums.length <= 104
  • -109 <= nums[i] <= 109

解法

使用 dp 数组,其中 dp[i] 存储的是以 nums[i] 结尾的最长连续递增子序列的长度。由于这是一个连续递增的序列,因此只需要比较当前元素与前一个元素的大小关系,然后决定是否更新 dp[i]

class Solution:
    def findLengthOfLCIS(self, nums: List[int]) -> int:
        # 初始化 dp 数组,其中每个元素都被设置为 1。
        # dp[i] 表示以 nums[i] 结尾的最长连续递增子序列的长度。
        dp = [1] * len(nums)
        
        # 从第二个元素开始遍历数组。
        for i in range(1, len(nums)):
            # 如果当前元素比前一个元素大,则连续递增子序列长度加 1。
            if nums[i] > nums[i-1]:
                dp[i] = dp[i-1] + 1  # 使用前一个状态来更新当前状态
        
        # 返回最长连续递增子序列的长度
        return max(dp)

Leetcode 718. 最长重复子数组

题目描述

给两个整数数组 nums1 和 nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度

 

示例 1:

输入: nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]
输出: 3
解释: 长度最长的公共子数组是 [3,2,1]

示例 2:

输入: nums1 = [0,0,0,0,0], nums2 = [0,0,0,0,0]
输出: 5

 

提示:

  • 1 <= nums1.length, nums2.length <= 1000
  • 0 <= nums1[i], nums2[i] <= 100

解法

  1. dp数组含义:以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i][j]。 (特别注意: “以下标i - 1为结尾的A” 标明一定是 以A[i-1]为结尾的字符串 )
  2. 递推公式:当A[i - 1] 和B[j - 1]相等的时候,dp[i][j] = dp[i - 1][j - 1] + 1;根据递推公式可以看出,遍历i 和 j 要从1开始!
  3. 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,正好符合递推公式逐步累加起来。
  4. 遍历顺序:外层for循环遍历A,内层for循环遍历B。同时题目要求长度最长的子数组的长度。所以在遍历的时候顺便把dp[i][j]的最大值记录下来。
  5. 打印dp数组

完整版代码如下:

class Solution:
    def findLength(self, nums1: List[int], nums2: List[int]) -> int:
        # m 和 n 是 nums1 和 nums2 的长度加 1,方便在 dp 数组中使用
        m, n = len(nums1) + 1, len(nums2) + 1

        # 初始化一个 m * n 的二维数组,所有元素都为 0
        dp = [[0] * n for _ in range(m)]

        # 初始化一个变量,用来存储最长公共子数组的长度
        max_length = 0

        # 从 1 开始遍历,因为 dp 数组的第一行和第一列都是 0,已经初始化
        for i in range(1, m):
            for j in range(1, n):
                # 如果 nums1 和 nums2 在 i-1 和 j-1 的位置上的元素相等
                if nums1[i - 1] == nums2[j - 1]:
                    # 更新 dp[i][j],它是左上角的 dp[i-1][j-1] 加上 1
                    dp[i][j] = dp[i - 1][j - 1] + 1

                    # 更新最大长度
                    max_length = max(dp[i][j], max_length)

        # 返回最大长度
        return max_length