300. 最长上升子序列

Posted 炫云云

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了300. 最长上升子序列相关的知识,希望对你有一定的参考价值。

300. 最长上升子序列

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

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

示例:

输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:

可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
你算法的时间复杂度应该为 O(n2) 。
进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?

动态规划

题目的意思是让我们从给定数组中挑选若干数字,这些数字满足: 如果 i < j 则 nums[i] < nums[j]。问:一次可以挑选最多满足条件的数字是多少个。

这种子序列求极值的题目,应该要考虑到贪心或者动态规划。这道题贪心是不可以的,我们考虑动态规划。

按照动态规划定义状态的套路,我们有两种常见的定义状态的方式:

  • d p [ i ] dp[i] dp[i] : 以 i i i 结尾(一定包括 i)所能形成的最长上升子序列长度, 答案是 m a x ( d p [ i ] ) max(dp[i]) max(dp[i]),其中 i = 0 , 1 , 2 , . . . , n − 1 i = 0,1,2, ..., n - 1 i=0,1,2,...,n1
  • d p [ i ] dp[i] dp[i] : 以 i i i 结尾(可能包括 i i i)所能形成的最长上升子序列长度,答案是 d p [ − 1 ] dp[-1] dp[1] (-1 表示最后一个元素)

容易看出第二种定义方式由于无需比较不同的 d p [ i ] dp[i] dp[i] 就可以获得答案,因此更加方便。但是想了下,状态转移方程会很不好写,因为 d p [ i ] dp[i] dp[i] 的末尾数字(最大的)可能是 任意 j < i j < i j<i 的位置。

第一种定义方式虽然需要比较不同的 d p [ i ] dp[i] dp[i] 从而获得结果,但是我们可以在循环的时候顺便得出,对复杂度不会有影响,只是代码多了一点而已。因此我们选择第一种建模方式

由于 d p [ j ] dp[j] dp[j] 中一定会包括 j j j,且以 j j j 结尾, 那么 n u m s [ j ] nums[j] nums[j] 一定是其所形成的序列中最大的元素,那么如果位于其后(意味着 i > j i > j i>j)的 n u m s [ i ] > n u m s [ j ] nums[i] > nums[j] nums[i]>nums[j],那么 n u m s [ i ] nums[i] nums[i] 一定能够融入 d p [ j ] dp[j] dp[j] 从而形成更大的序列,这个序列的长度是 d p [ j ] + 1 dp[j] + 1 dp[j]+1。因此状态转移方程就有了:dp[i] = dp[j] + 1 (其中 i > j, nums[i] > nums[j])

[10, 9, 2, 5, 3, 7, 101, 18] 为例,当我们计算到 d p [ 5 ] dp[5] dp[5]的时候,我们需要往回和 0,1,2,3,4 进行比较。

具体的比较内容是:

最后从三个中选一个最大的 **+ 1 赋给 dp[5]**即可。

复杂度分析

  • 时间复杂度: O ( N 2 ) O(N ^ 2) O(N2)
  • 空间复杂度: O ( N ) O(N) O(N)
class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        n = len(nums)
        if n == 0: return 0
        dp = [1] * n
        ans = 1
        for i in range(n):
            for j in range(i):
                if nums[i] > nums[j]:
                    dp[i] = max(dp[i], dp[j] + 1)
                    ans = max(ans, dp[i])
        return  ans

贪心 + 二分查找

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

基于上面的贪心思路,我们维护一个数组 d [ i ] d[i] d[i]​​, 表示长度为 i + 1 i+1 i+1​​ 的最长上升子序列的末尾元素的最小值, 用 l e n l e n len​​ 记录目前最长上升子序列的长度, 起始时 len 为 1 , d [ 0 ] = n u m s [ 0 ] 1, d[0]=n u m s[0] 1,d[0]=nums[0]​​ 。

同时我们可以注意到 d [ i ] d[i] d[i] 是关于 i i i 单调递增的。

我们依次遍历数组 n u m s n u m s nums​​​​ 中的每个元素,并更新数组 d d d​​​​ 和 l e n l e n len​​​​ 的值。

  • 如果 n u m s [ i ] > d [ l e n − 1 ] n u m s[i]>d[l e n-1] nums[i]>d[len1] 则更新 len = l e n + 1 =l e n+1 =len+1​, d.append(nums_i)
  • 否则在 d [ 0 … l e n − 1 ] d[0 \\ldots l e n-1] d[0len1]​​​​​​​​​ 中找满足 n u m s [ i ] < d [ k ] n u m s[i]<d[k] nums[i]<d[k]​​​​​​​​​ 的下标 k k k​​​​​​​​​, 并更新 d [ k ] = d[k]= d[k]=​​​​​​​​​ nums [ i ] 。  [i]_{\\text {。 }} [i] ​​​​​​​​​ :

根据 d d d​​ 数组的单调性,我们可以使用二分查找寻找下标 k k k​​​​, 优化时间复杂度。 最后整个算法流程为:

  • 设当前已求出的最长上升子序列的长度为 l e n l e n len​ (初始时为 1 ) , 从前往后遍历数组 nums, 在遍历到 n u m s [ i ] n u m s[i] nums[i]​ 时:
  • 如果 n u m s [ i ] > d [ l e n − 1 ] n u m s[i]>d[l e n-1] nums[i]>d[len1]​, 则直接加入到 d d d​ 数组末尾,并更新 len = l e n + 1 =l e n+1 =len+1​;
  • 否则,在 d d d​ 数组中二分查找,找到第一个比 nums [ i ] [i] [i]​ 大的数 d [ k ] d[k] d[k]​, 并更新 d [ k ] = d[k]= d[k]=​ nums [ i ] 。  [i]_{\\text {。 }} [i] 
class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        d = []
        for nums_i in nums:
            if not d or nums_i > d[-1]:
                d.append(nums_i)
            else:
                left ,right =0 , len(d)-1
                k = right
                while left <= right:
                    mid = left +( right-left) // 2
                    if d[mid] >= nums_i: # K 在[left ,mid-1 ]
                        k = mid
                        right = mid-1
                    else:
                        left = mid +1
                d[k] = nums_i
        return len(d)
class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        d = []
        for nums_i in nums:
            if not d or nums_i > d[-1]:
                d.append(nums_i)
            else:
                left ,right =0 , len(d)-1
                while left <right:
                    mid = left +( right-left) // 2
                    if d[mid] >= nums_i: # K 在[left ,mid ]
                        right = mid
                    else:
                        left = mid +1
                d[left] = nums_i # k == left
        return len(d)

相关题目

倒卖战利品

以上是关于300. 最长上升子序列的主要内容,如果未能解决你的问题,请参考以下文章

leetcode300 最长上升子序列(Medium)

Leetcode 300.最长上升子序列

300. 最长上升子序列

300. 最长上升子序列

300. 最长上升子序列

Leetcode-300. 最长上升子序列