经典动态规划题--最长递增子序列

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

老动态规划了

设 dp[i] 是以 nums[i] 为结尾的最长严格递增子序列的长度,则:
dp[i] = max(dp[j]+1), 0 <= j < i

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        dp = []
        for i in range(len(nums)):
            dp.append(1)
            for j in range(i):
                if nums[i]>nums[j]:
                    dp[i] = max(dp[j]+1, dp[i])
        return max(dp)

官方题解中还使用了贪心+二分查找法,时间复杂度更低:

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        d = []
        for n in nums:
            if not d or n > d[-1]:
                d.append(n)
            else:
                l, r = 0, len(d) - 1
                loc = r
                while l <= r:
                    mid = (l + r) // 2
                    if d[mid] >= n:
                        loc = mid
                        r = mid - 1
                    else:
                        l = mid + 1
                d[loc] = n
        return len(d)

考虑一个简单的贪心,如果我们要使上升子序列尽可能的长,则我们需要让序列上升得尽可能慢,因此我们希望每次在上升子序列最后加上的那个数尽可能的小。

基于上面的贪心思路,我们维护一个数组 d[i]d[i]d[i] ,表示长度为 iii 的最长上升子序列的末尾元素的最小值,用 len\textit{len}len 记录目前最长上升子序列的长度,起始时 lenlenlen 为 111,d[1]=nums[0]d[1] = \textit{nums}[0]d[1]=nums[0]。

同时我们可以注意到 d[i]d[i]d[i] 是关于 iii 单调递增的。因为如果 d[j]≥d[i]d[j] \geq d[i]d[j]≥d[i] 且 j<ij < ij<i,我们考虑从长度为 iii 的最长上升子序列的末尾删除 i−ji-ji−j 个元素,那么这个序列长度变为 jjj ,且第 jjj 个元素 xxx(末尾元素)必然小于 d[i]d[i]d[i],也就小于 d[j]d[j]d[j]。那么我们就找到了一个长度为 jjj 的最长上升子序列,并且末尾元素比 d[j]d[j]d[j] 小,从而产生了矛盾。因此数组 ddd 的单调性得证。

我们依次遍历数组 nums\textit{nums}nums 中的每个元素,并更新数组 ddd 和 lenlenlen 的值。如果 nums[i]>d[len]\textit{nums}[i] > d[\textit{len}]nums[i]>d[len] 则更新 len=len+1len = len + 1len=len+1,否则在 d[1…len]d[1 \ldots len]d[1…len]中找满足 d[i−1]<nums[j]<d[i]d[i - 1] < \textit{nums}[j] < d[i]d[i−1]<nums[j]<d[i] 的下标 iii,并更新 d[i]=nums[j]d[i] = \textit{nums}[j]d[i]=nums[j]。

根据 ddd 数组的单调性,我们可以使用二分查找寻找下标 iii,优化时间复杂度。

最后整个算法流程为:

  • 设当前已求出的最长上升子序列的长度为 len\textit{len}len(初始时为 111),从前往后遍历数组 nums\textit{nums}nums,在遍历到 nums[i]\textit{nums}[i]nums[i] 时:

  • 如果 nums[i]>d[len]\textit{nums}[i] > d[\textit{len}]nums[i]>d[len] ,则直接加入到 ddd 数组末尾,并更新 len=len+1\textit{len} = \textit{len} + 1len=len+1;

  • 否则,在 ddd 数组中二分查找,找到第一个比 nums[i]\textit{nums}[i]nums[i] 小的数 d[k]d[k]d[k] ,并更新 d[k+1]=nums[i]d[k + 1] = \textit{nums}[i]d[k+1]=nums[i]。

以输入序列 [0,8,4,12,2][0, 8, 4, 12, 2][0,8,4,12,2] 为例:

第一步插入 000,d=[0]d = [0]d=[0];

第二步插入 888,d=[0,8]d = [0, 8]d=[0,8];

第三步插入 444,d=[0,4]d = [0, 4]d=[0,4];

第四步插入 121212,d=[0,4,12]d = [0, 4, 12]d=[0,4,12];

第五步插入 222,d=[0,2,12]d = [0, 2, 12]d=[0,2,12]。

最终得到最大递增子序列长度为 333。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值