代码随想录算法训练营Day52 | 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]

    109253710118
    11122344
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]

    12321
    300100
    201020
    110003
    400000
    700000
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 数组定义,在解题的书写上的确更加简洁,不需要进行特殊的初始化。但我还是觉得以上的解法更加符合直觉。

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值