10.4.1 (python) 动态规划专题之最长递增子序列 —— Longest Increasing Subsequence & Russian Doll Envelopes

Longest Increasing Subsequence 简称 LIS,是一个经典问题。我们看一下经典解题方法及一道应用题目。

 

300. Longest Increasing Subsequence

Given an unsorted array of integers, find the length of longest increasing subsequence.

Example:

Input: [10,9,2,5,3,7,101,18]
Output: 4 
Explanation: The longest increasing subsequence is [2,3,7,101], therefore the length is 4. 

Note:

  • There may be more than one LIS combination, it is only necessary for you to return the length.
  • Your algorithm should run in O(n2) complexity.

题目解析:

我们摘网上的解答,两种DP解法,逐渐去理解。点击链接可以看原文,有图解。

动态规划1  

  • 使用数组 dp 保存每步子问题的最优解。
  • dp[i] 代表含第 i 个元素的最长上升子序列的长度。
  • 求解 dp[i] 时,向前遍历 j∈[0,i)
  • 当 nums[i] > nums[j] 时: nums[i]可以接在 nums[j] 之后(此题要求严格递增),此情况下最长上升子序列长度为 dp[j] + 1;计算出的 dp[j] + 1 的最大值,为直到 i的最长上升子序列长度(即 dp[i] )。
  • 返回 dp 列表最大值,即可得到全局最长上升子序列长度

复杂度分析:
时间复杂度 O(N^2) 双层遍历,空间复杂度 O(N)

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        if not nums: return 0
        dp = [1] * len(nums)
        for i in range(len(nums)):
            for j in range(i):
                if nums[j] < nums[i]: # 如果要求非严格递增,将此行 '<' 改为 '<=' 即可。
                    dp[i] = max(dp[i], dp[j] + 1)
        return max(dp)

动态规划 + 二分查找

上面的方法比较常规,应该容易理解。而下面这种方法不太容易想到,理解起来也有一定难度,但是使用简便。

数组 tail,用于保存最长上升子序列,(需要注意tail也不是真实的结果,看下面的内容慢慢理解)其中每个元素 tails[k]的值代表 长度为 k+1的子序列尾部元素的值

对原序列进行遍历,将每位元素二分插入 tail 中。

如果 tail 中元素都比它小,将它插到最后
否则,用它覆盖掉比它大的元素中最小的那个。
总之,思想就是让 tail 中存储比较小的元素。这样,tail 未必是真实的最长上升子序列,但长度是对的。

再解释一下:

在遍历数组过程中,不断更新tail,始终保持每个尾部元素值最小 。我觉得我们需要一个例子,例如 [1,5,3,4], 遍历到元素 5 时,tail = [1,5]此时tail长度为2,尾部元素为5;当遍历到元素 3 时,尾部元素值应更新至 3,因为 3 遇到比它大的数字的几率更大),此时tail=[1,3],长度为2,但是对应的LIS可能有两种情况[1,5]/[1,3]。我们继续遍历到元素4,此时4比tail[-1]还大,tail更新为[1,3,4],LIS=3,但是往往tail里的序列不一定是真实情况。

通过这个例子加深理解,遍历时主要考虑两种情况:

tail中最大的tail[-1] < 当前,意味着 nums[k] 可以接在前面所有长度的子序列之后,新子序列长度+1;

否则 当前元素 < tail[-1],我们需在tail中找到比它大的元素中最小的元素的位置,替换。

下面两份代码是等价的,第二个只是调用了二分查找的库罢了。

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        if not nums:
            return 0
        def bs(arr, x):
            lt, rt = 0, len(arr)
            while lt < rt:
                mid = (lt + rt) // 2
                if arr[mid] < x:
                    lt = mid + 1
                else:
                    rt = mid
            return rt
            
        ret = [nums[0]]        
        for n in nums[1:]:
            if n > ret[-1]:
                ret.append(n)
            else:
                ix = bs(ret, n)
                ret[ix] = n
        
        return len(ret)
class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        # dp[i] is the minimal possible tail of sequences of length (i+1)
        dp = []
        for x in nums:
            if not dp or dp[-1]<x: dp.append(x)
            else: dp[bisect.bisect_left(dp,x)]=x
        return len(dp)

我们看一道具体的应用题目。

354. Russian Doll Envelopes

You have a number of envelopes with widths and heights given as a pair of integers (w, h). One envelope can fit into another if and only if both the width and height of one envelope is greater than the width and height of the other envelope.

What is the maximum number of envelopes can you Russian doll? (put one inside other)

题目解析:

俄罗斯套娃~

首先等于告诉你了这道题是考察最长递增子序列。。没办法谁让这是专题介绍。自己能想出这是LIS才是厉害哈。。

那么先理解一下为什么是这样的吧。

我们让信封按宽由小到大,然后找出宽递增的情况下高度也递增的子序列。这样后一个才可以装下前一个。

思路就是让信封按宽递增,按高递减,看代码自行理解吧。

class Solution:
    def maxEnvelopes(self, envelopes: List[List[int]]) -> int:
        n = len(envelopes)
        if n <= 1:
            return n
        envelopes = sorted(envelopes, key=lambda x: (x[0], -x[1]))         
        dp = []
        dp.append(envelopes[0][1])
        for i in range(1, n):
            if envelopes[i][1] > dp[-1]:
                dp.append(envelopes[i][1])
            else:
                ix = bisect.bisect_left(dp, envelopes[i][1])
                dp[ix] = envelopes[i][1]
        return len(dp)

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值