LeetCode 300. 最长上升子序列 | Python

300. 最长上升子序列


题目来源:力扣(LeetCode)https://leetcode-cn.com/problems/longest-increasing-subsequence

题目


给定一个无序的整数数组,找到其中最长上升子序列的长度。

示例:

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

说明:

  • 可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。

  • 你算法的时间复杂度应该为 O ( n 2 ) O(n^2) O(n2)

进阶: 你能将算法的时间复杂度降低到 O ( n log ⁡ n ) O(n \log n) O(nlogn) 吗?

解题思路


思路:动态规划、二分查找

在这里,题目要求找到给定无序数组中最长上升子序列的长度。

看示例部分,我们可以发现,这里要求返回的序列并非要求要连续的。

动态规划

这里我们用动态规划的方法来解决这问题。

状态定义

dp[i] 为以 nums[i] 结尾的上升子序列的长度。

状态转移方程

在这里,我们要求 dp[i] 之前,需要先求得 dp[0...i-1] 的值。

然后只要 nums[i]的值严格大于 i 前面的某个数,那么 num[i] 便可以跟在这个数后面,形成更长的子序列。

也就是说 dp[i] 就等于 i 前面严格小于 num[i]dp 值最大 + 1。

那么状态方程如下:
d p [ i ] = m a x ( d p [ j ] ) + 1 dp[i]=max(dp[j])+1 dp[i]=max(dp[j])+1
其中 0 ≤ j < i 0\leq j < i 0j<i,并且 n u m [ i ] > n u m [ j ] num[i] > num[j] num[i]>num[j]

状态初始化

因为单独一个数字也能算作是一个长度为 1 的上升序列,那么先将 dp 数组初始化为 1。

注意:

这里不能直接返回 dp 数组的最后一个状态值,这里要求返回最长的上升子序列,那么最终要返回 dp 数组中的最大值。

具体的代码实现如下:

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        if not nums:
            return 0
            
        n = len(nums)

        dp = [1] * n
        
        for i in range(n):
            for j in range(i):
                if nums[i] > nums[j]:
                    dp[i] = max(dp[i], dp[j]+1)
        # 返回 dp 数组最大值
        return max(dp)

复杂度分析:

  • 时间复杂度: O ( N 2 ) O(N^2) O(N2) N N N 为数组 nums 的长度。两层遍历都为线性时间 O ( N ) O(N) O(N)
  • 空间复杂度: O ( N ) O(N) O(N),额外使用长度为 N N Ndp 数组。
动态规划+二分查找

进阶部分,希望时间复杂度能够降到 O ( N log ⁡ N ) O(N \log N) O(NlogN)。下面就前面的方法进行优化。

我们可以观察示例中的上升子序列,如果需要获得更长的子序列,那么子序列上升的幅度越小越好。

也就是,当上升子序列中最后的元素越小,那么与后面的元素连接形成更长的子序列的可能性更大。

状态定义

这里要重新定义状态。设 tail[i] 表示长度为 i+1 子序列尾部元素值。

状态转移方程

在这里 tail 列表一定是要严格递增的。这里用反证法来证明:

证明: i < j i < j i<j 时,都有 t a i l [ i ] < t a i l [ j ] tail[i] < tail[j] tail[i]<tail[j]

反证法:

假设 i < j i<j i<j 时,存在 t a i l [ i ] > = t a i l [ j ] tail[i]>= tail[j] tail[i]>=tail[j]

这里 t a i l [ i ] tail[i] tail[i] 表示长度为 i + 1 i+1 i+1 子序列尾部元素值, t a i l [ j ] tail[j] tail[j] 同理。

也就是说,上面的假设中,较短子序列中的尾部元素值 要大于 较长子序列中的尾部元素值

现在,将 较长子序列 从尾部开始删减元素,直至长度等于 较短子序列 的长度( i + 1 i+1 i+1)。假设在这个缩减后长度为 i + 1 i+1 i+1 的序列尾部元素值为 t a i l [ k ] tail[k] tail[k],那么此时 t a i l [ k ] < t a i l [ j ] tail[k] < tail[j] tail[k]<tail[j] ,也就是说长度为 i + 1 i+1 i+1 的子序列的尾部元素值更小,那么这就跟原假设 t a i l [ i ] > = t a i l [ j ] tail[i]>= tail[j] tail[i]>=tail[j] 矛盾。

故, i < j i < j i<j 时,都有 t a i l [ i ] < t a i l [ j ] tail[i] < tail[j] tail[i]<tail[j]

转移方程

lengthtail​ 数组的长度。遍历数组 nums,假设当前元素为nums[i],这里可能出现的情况:

  • nums[i] 大于 tail 数组尾部元素,那么可以将 num[i] 直接放到 tail 数组尾部,更新长度 length,令 length +1
  • 若不存在上面的情况,那么则需要通过二分查找在 tail 数组中找到第一个比 nums[i] 大的元素,用 nums[i] 替换。
状态初始化

初始化 tail 数组值为 0。

tail[0]=nums[0],初始长度 length 为 1。

具体的代码实现如下:

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        n = len(nums)
        # 处理数组为空的情况
        if not nums:
            return n
        
        # 一般情况,初始化 tail 数组值都为 0
        tail = [0] * n
        # 令 tail[0] = nums[0],length 长度为 1
        tail[0] = nums[0]
        length = 1

        # 开始遍历
        for i in range(1, n):
            # 如果出现 nums[i] 大于 tail 数组末尾元素
            # 将 nums[i] 放到末尾,更新 length
            if nums[i] > tail[length-1]:
                tail[length] = nums[i]
                length+=1
                continue
            
            # nums[i] 不大于 tail 数组末尾元素时
            # 利用二分查找在 tail 数组索引为 [0 , length) 之间的元素找到第一个大于 nums[i] 的元素
            # 将其替换为 nums[i]
            left = 0
            right = length-1
            while left < right:
                mid = left + (right-left) // 2
                if nums[i] > tail[mid]:
                    left = mid + 1
                else:
                    right = mid
            tail[left] = nums[i]
        
        return length

复杂度分析:

  • 时间复杂度: O ( N log ⁡ N ) O(N\log N) O(NlogN) N N Nnums 数组的长度,遍历 nums 数组需要 O ( N ) O(N) O(N)。遍历查找元素使用二分查找需要 O ( log ⁡ N ) O(\log N) O(logN),所以总时间为 O ( N log ⁡ N ) O(N\log N) O(NlogN)
  • 空间复杂度: O ( N ) O(N) O(N)tail 数组的开销。

欢迎关注


公众号 【书所集录


如有错误,烦请指出,欢迎指点交流。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值