代码随想录算法训练营Day52 | 300. 最长递增子序列 | 674. 最长连续递增序列 | 718. 最长重复子数组

文章目录

  • 300. 最长递增子序列
  • 674. 最长连续递增序列
    • 贪心解法
  • 718. 最长重复子数组

300. 最长递增子序列

题目链接 | 解题思路

本题问的是子序列,定义为“由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序”。
作为 dp 的序列第一题,难度不高,最大的难点在于找到正确的 dp 数组定义。这似乎也是序列 dp 的难点,和之前(背包问题,遍历顺序)、(股票问题、dp 状态)以及一些递推公式的难点都不一样。

  1. dp 数组的下标含义:dp[i] 代表了 nums[0, i+1] 中以 nums[i] 结尾的最长递增子序列的长度

    • 这个定义中“以 nums[i] 结尾”似乎有些奇怪,类似于股票问题里“指定的当前天的状态下,最优解是什么”,只不过这里指定的是最长递增子序列的结尾。
    • 原因在于,想要通过 nums[i] 与之前的值 nums[j] ( j < k ) (j < k) (j<k) 进行比较从而得到新的最长递增子序列的长度,就必须有所限定。否则,在不知道之前所得到的最长递增子序列的最后一个元素的情况下,无法判断当前元素是否能跟上之前的序列。
  2. dp 递推公式:

    if nums[i] > nums[j]:
    	dp[i] = max(dp[i], dp[j] + 1)
    
    • 这个递推公式也非常特殊,因为要求遍历之前所有的下标 j < i,从而找到以 nums[i] 结尾的最长递增子序列,而非以前的递推公式通常只依赖之前的某个状态一次
  3. dp 数组的初始化:仍然非常特殊,初始化都为 1 而不是 0,因为根据题目定义,就算是当前值 nums[i] = min(nums[:i]),也至少能得到一个长度为 1 的递增子序列

  4. 遍历顺序:从前向后遍历,根据递推公式不难得到内部的比较范围是 [0, i-1]

  5. 举例推导:nums = [10,9,2,5,3,7,101,18]

    10 9 2 5 3 7 101 18
    1 1 1 2 2 3 4 4
class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        # dp[i] represents the max length of subsequence in nums[:i+1] and including nums[i]
        dp = [1] * len(nums)

        for i in range(1, len(nums)):
            for j in range(i-1, -1, -1):
                if nums[i] > nums[j] and dp[j] >= dp[i]:
                    dp[i] = dp[j] + 1
        
        return max(dp)

值得注意的是,其实可以通过保持最大结果的方式避免最后的 max 操作,但不会对复杂度产生优化,所以可以偷懒这么写方便一点。

dp 数组的定义终于成为了一大梦魇!

674. 最长连续递增序列

题目链接 | 解题思路

本题和上一题唯一的区别在于连续子序列,这个区别导致的变化是递推公式变得简单了。由于要求子序列必须连续,所以在比较 nums[i] 时只需要和 nums[i-1] 比较即可,否则直接重置当前连续子序列的长度。

class Solution:
    def findLengthOfLCIS(self, nums: List[int]) -> int:
        # dp[i] represents the max length of continuous subarray ending in nums[i]
        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)

贪心解法

class Solution:
    def findLengthOfLCIS(self, nums: List[int]) -> int:
        result = 1
        curr_max = 1

        for i in range(1, len(nums)):
            if nums[i] > nums[i-1]:
                curr_max += 1
            else:
                curr_max = 1
            result = max(result, curr_max)
        
        return result

718. 最长重复子数组

题目链接 | 解题思路

本题最难的地方在于找到正确的 dp 数组定义。一些思路的小提示:之前两题的定义都是需要以当前元素结尾的最大子序列长度,而本题有两个输入数组,所以使用二维数组来定义 dp 似乎也很合理。

  1. dp 数组的下标含义:dp[i][j] 代表着 nums1[: i+1]nums2[: j+1] 所具有的nums1[i]nums2[j]结尾的最长重复子序列的长度

    • “以当前元素结尾”这个条件反复在之前的定义中出现,因为只有这个条件才能让人充分利用当前元素 nums1[i]nums2[j]
    • 但这也意味着最后要得到结果,还需要遍历整个二维 dp 数组,而不能像以前的题型一样直接取 dp[-1][-1]
  2. dp 递推公式:

    if nums1[i] == nums2[j]:
    	dp[i][j] = dp[i-1][j-1] + 1
    
    • 根据定义,只有当两个连续子数组的最后一个元素相等时,才能从子状态 dp[i-1][j-1] 推导出当前 dp[i][j]
    • 如果两个连续子数组的最后一个元素都不相等,根据定义,显然dp[i][j]=0
  3. dp 数组的初始化:由于递推公式中用到了 i-1,我们显然需要初始化第一行 dp[i][0] 和第一列 dp[0][j]

    • 根据定义,dp[i][0] 记录了在以 nums1[i] 为结尾的连续子数组和 nums2[:1] 的最长重复子序列,由于 nums2[:1] 的长度为 1,如果 nums1[i] == nums2[0],则 dp[i][0]=1,否则初始化为 0
    • dp[0][j] 同理
    • 其他值全部初始化为 0 即可
  4. dp 的遍历顺序:从小到大即可,由于只需要左上角的元素进行过初始化,两层循环的嵌套也就无所谓内外顺序

  5. 举例推导:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]

    1 2 3 2 1
    3 0 0 1 0 0
    2 0 1 0 2 0
    1 1 0 0 0 3
    4 0 0 0 0 0
    7 0 0 0 0 0
class Solution:
    def findLength(self, nums1: List[int], nums2: List[int]) -> int:
        result = 0
        dp = [[0] * len(nums2) for _ in range(len(nums1))]

        for j in range(len(nums2)):
            if nums1[0] == nums2[j]:
                dp[0][j] = 1
                result = 1
            
        for i in range(len(nums1)):
            if nums1[i] == nums2[0]:
                dp[i][0] = 1
                result = 1
           
        for i in range(1, len(nums1)):
            for j in range(1, len(nums2)):
                if nums1[i] == nums2[j]:
                    dp[i][j] = dp[i-1][j-1] + 1
                if dp[i][j] > result:
                    result = dp[i][j]
         
        return result

代码随想录上提供了略有差异的 dp 数组定义,在解题的书写上的确更加简洁,不需要进行特殊的初始化。但我还是觉得以上的解法更加符合直觉。

另外,注意到递归中实际上只依赖于前一层的状态,所以就像背包问题一样,也可以把二维数组压成滚动数组来求解。由于需要保持之前的状态,滚动数组的解需要内层遍历从后向前进行。

你可能感兴趣的:(代码随想录算法训练营一刷,算法)