300. 最长上升子序列

一、300. 最长上升子序列

1.1、题目描述

在这里插入图片描述

1.2.1、粗暴法(超时)

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        return self.recursive(nums, float('-inf'), 0)

    def recursive(self, nums: List[int], preV: int, curPos: int) -> int:
        if curPos == len(nums):
            return 0

        token = 0
        if nums[curPos] > preV:
            token = 1 + self.recursive(nums, nums[curPos], curPos+1)
        
        notoken = self.recursive(nums, preV, curPos+1)

        return max(token, notoken)

1.2.2、带记忆的递归(超时)

class Solution:
    def __init__(self) -> None:
        self.memo = None

    def lengthOfLIS(self, nums: List[int]) -> int:
        n = len(nums)
        self.memo = [[-1]*n for _ in range(n)]

        return self.recursive(nums, -1, 0)

    def recursive(self, nums: List[int], preIdx: int, curPos: int) -> int:
        if curPos == len(nums):
            return 0
        
        if self.memo[preIdx+1][curPos] >= 0:
            return self.memo[preIdx+1][curPos] 

        token = 0
        if preIdx < 0 or nums[curPos] > nums[preIdx]:
            token = 1 + self.recursive(nums, curPos, curPos+1)
        
        notoken = self.recursive(nums, preIdx, curPos+1)
        self.memo[preIdx+1][curPos] = max(token, notoken)

        return max(token, notoken)

1.2.3、动态规划(dp[i] 的值代表 nums 前 i 个数字的最长子序列长度。)

转移方程: dp[i] = max(dp[i], dp[j] + 1) for j in [0, i)。
在这里插入图片描述

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

        n = len(nums)
        dp = [1]*n  # 初始化,每个元素都是一个单独子序列

        # dp[i] 的值代表 nums 前 i 个数字的最长子序列长度。
        for i in range(n):
            for j in range(i):
                if nums[j] < nums[i]:
                    dp[i] = max(dp[i], dp[j]+1)  # 如果要求非严格递增,将此行 '<' 改为 '<=' 即可。
        return max(dp)

1.2.4、动态规划 + 二分查找

解题思路:

  • 降低复杂度切入点: 解法一中,遍历计算 dp 列表需 O(N),计算每个 dp[k] 需 O(N)。
  1. 动态规划中,通过线性遍历来计算 dp 的复杂度无法降低;
  2. 每轮计算中,需要通过线性遍历 [0,k) 区间元素来得到 dp[k] 。我们考虑:是否可以通过重新设计状态定义,使整个 dp 为一个排序列表;这样在计算每个 dp[k] 时,就可以通过二分法遍历 [0,k) 区间元素,将此部分复杂度由 O(N) 降至 O(logN)。
    设计思路:
  • 新的状态定义:

    • 我们考虑维护一个列表 tails,其中每个元素 tails[k]的值代表 长度为 k+1的子序列尾部元素的值。
      如 [1,4,6] 序列,长度为 1,2,3 的子序列尾部元素值分别为tails=[1,4,6]。
  • 状态转移设计:

    • 设常量数字 N,和随机数字 x,我们可以容易推出:当 N 越小时,N<x的几率越大。例如: N=0肯定比 N=1000 更可能满足 N<x。
    • 在遍历计算每个 tails[k],不断更新长度为 [1,k] 的子序列尾部元素值,始终保持每个尾部元素值最小 (例如 [1,5,3]], 遍历到元素 5 时,长度为 2 的子序列尾部元素值为 5;当遍历到元素 3 时,尾部元素值应更新至 3,因为 3 遇到比它大的数字的几率更大)。
    • tails列表一定是严格递增的: 即当尽可能使每个子序列尾部元素值最小的前提下,子序列越长,其序列尾部元素值一定更大。
      • 反证法证明: 当 k < i,若tails[k]>=tails[i],代表较短子序列的尾部元素的值 > 较长子序列的尾部元素的值。这是不可能的,因为从长度为 i 的子序列尾部倒序删除 i−1 个元素,剩下的为长度为 k 的子序列,设此序列尾部元素值为 v,则一定有v<tails[i] (即长度为 k 的子序列尾部元素值一定更小), 这和tails[k]>=tails[i] 矛盾。
      • 既然严格递增,每轮计算 tails[k]时就可以使用二分法查找需要更新的尾部元素值的对应索引 i。
  • 算法流程:

    • 状态定义:

      • tails[k] 的值代表 长度为 k+1 子序列 的尾部元素值。
    • 转移方程: 设 res 为 tails当前长度,代表直到当前的最长上升子序列长度。设 j∈[0,res),考虑每轮遍历 nums[k] 时,通过二分法遍历[0,res) 列表区间,找出nums[k] 的大小分界点,会出现两种情况:

      • 区间中存在 tails[i] > nums[k]: 将第一个满足 tails[i] > nums[k] 执行 tails[i] = nums[k] ;因为更小的 nums[k] 后更可能接一个比它大的数字(前面分析过)。
      • 区间中不存在 tails[i]>nums[k] : 意味着nums[k] 可以接在前面所有长度的子序列之后,因此肯定是接到最长的后面(长度为 res),新子序列长度为res+1。
    • 初始状态:

      • 令 tails列表所有值=0。
    • 返回值:

      • 返回 res ,即最长上升子子序列长度。
    • 复杂度分析:

      • 时间复杂度 O(NlogN): 遍历 nums 列表需 O(N),在每个 nums[i] 二分法需 O(logN)。
      • 空间复杂度 O(N): tails列表占用线性大小额外空间。
class Solution:
    def lengthOfLIS(self, nums: [int]) -> int:
        tails, res = [0] * len(nums), 0
        for num in nums:
            i, j = 0, res
            while i < j:
                m = (i + j) // 2
                if tails[m] < num: i = m + 1 # 如果要求非严格递增,将此行 '<' 改为 '<=' 即可。
                else: j = m
            tails[i] = num
            if j == res: res += 1
        return res

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值