Skip to content

300. 最长递增子序列

题目

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

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

示例 1:

输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

示例 2:

输入:nums = [0,1,0,3,2,3]
输出:4

示例 3:

输入:nums = [7,7,7,7,7,7,7]
输出:1

提示:

  • 1 <= nums.length <= 2500
  • -10^4 <= nums[i] <= 10^4

进阶:

  • 你能将算法的时间复杂度降低到 O(n log(n)) 吗?

解答

思路:子集型回溯

有选或不选以及枚举选哪个两种思路。

  • 选或不选:为了比较大小,需要知道上一个所选的数字,两个参数
  • 枚举选哪个:枚举到 num[i] 时,只需要比较它和前面数字的大小,所有比它小的都可以成为候选

我们以 枚举选哪个 为例子:

  • 当前操作:枚举 nums[j],其中 j < i and nums[j] < nums[i]
  • 当前问题:以 nums[i] 结尾的 最长递增子序列 Longest Increasing Subsequence,LIS 长度
  • 它的子问题:以 nums[j] 结尾的 LIS 长度

递归关系式:

dfs(i)=max(dfs(j))+1,j<i,nums[j]<nums[i]

注意枚举选哪个这种情况下,dfs(0)..dfs(n - 1) 都可能是答案,也就是以 nums[i] 结尾的 LIS 取最大值。

这里有 O(n) 个状态,每个状态需要 O(n) 的时间:

  • 时间复杂度为 O(n2)
  • 空间复杂度为 O(n)

代码

python
class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        n = len(nums)

        @cache
        def dfs(i):
            res = 0
            
            for j in range(i):
                if nums[j] < nums[i]:
                    res = max(res, dfs(j))
            
            return res + 1
        
        ans = 0
        for i in range(n):
            ans = max(ans, dfs(i))
        
        return ans # max(dfs(i) for i in range(n))

思路:递推

f[i]=max(f[j])+1,,j<i,nums[j]<nums[i]

二重循环:

  • 时间复杂度为 O(n2)
  • 空间复杂度为 O(n)

代码

python
class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        n = len(nums)
        f = [0] * n

        for i in range(n):
            for j in range(i):
                if nums[j] < nums[i]:
                    f[i] = max(f[i], f[j])
            f[i] += 1
        
        return max(f)

思路:参考 LCS

nums 的 LIS 等价于 nums排序去重后nums 的 LCS。这是因为 LIS 一定是 排序去重后nums 的一个子序列,它同时也是 nums 的一个子序列,因此它们的 LCS 一定是 LIS。

可惜这种做法也是 O(mn) 的。

代码

python
class Solution:
    def longestCommonSubsequence(self, s: str, t: str) -> int:
        n = len(s)
        m = len(t)

        f = [0] * (m + 1)

        for x in s:
            pre = f[0]
            for j, y in enumerate(t):
                tmp = f[j + 1] # 上一轮的 f[i][j]

                if x == y:
                    f[j + 1] = pre + 1
                else:
                    f[j + 1] = max(f[j + 1], f[j])
                
                pre = tmp

        return f[m]

    def lengthOfLIS(self, nums: List[int]) -> int:
        nums_sort = sorted(set(nums))

        return self.longestCommonSubsequence(nums, nums_sort)

思路:优化时间复杂度

动态规划想要优化时间复杂度,有个技巧就是:交换状态和状态值

  • f[i] 表示 末尾元素nums[i] 的 LIS 长度
  • g[i] 表示 长度i + 1 的 IS 的 末尾元素 的最小值(它足够小才能方便扩展)
python
nums 
  = [1, 6, 7, 2, 4, 5, 3]
g = [1]
g = [1, 6]
g = [1, 6, 7]
g = [1, 2, 7]    # 长度为 2 的 IS 为 1, 2
g = [1, 2, 4]    # 长度为 3 的 IS 为 1, 2, 4
g = [1, 2, 4, 5]
g = [1, 2, 3, 5] # 长度为 3 的 IS 为 1, 2, 3

这种由于没有重叠子问题,只是不断通过新的 nums[i] 来更新 g 数组,因此不是动态规划,而是一个贪心问题。

我们接下来证明 g 数组的一些性质:

  • g严格递增
  • 每一次只能更新一个位置
  • 更新的位置是 第一个 >= nums[i] 的数的下标

如果 g 不是严格递增的,那么存在 g[i] >= g[j], i < j,那么 g[j] 所对应的 IS 中的第 i 个数就应该满足 < g[j] <= g[i],而这与 g[i] 的定义矛盾。

假设一次更新了两个位置,说明 nums[i] 导致 g 中的两个位置变成了 nums[i],而这与严格递增是矛盾的,因此每一次只能通过 nums[i] 更新 g 中的一个位置。

同时由于我们是从左到右遍历的,因此 nums[i] 一定可以和 g 中的数构成 IS,也就是一定会被添加进去。添加的位置只需要遍历 g(可以二分),找到 第一个 >= nums[i] 的数:

  • 对于更大的数:不能替换,否则违背严格递增
  • 对于更小的数:不能替换,否则违背末尾元素最小的要求
  • 因此只能是第一个满足 >= nums[i] 的数

由于 g 恰好递增,因此可以使用二分查找第一个 >= nums[i] 的数的下标 j,如果 j 不存在,那么把 nums[i] 直接添加到 g 的末尾,否则修改 g[j] = nums[i]

一次遍历,每次都需要 O(logn) 的时间二分查找,因此:

  • 时间复杂度为 O(nlogn)
  • 空间复杂度为 O(n)

代码

python
class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        g = []
        for x in nums:
            j = bisect_left(g, x)
            
            if j == len(g):
                g.append(x)
            else:
                g[j] = x
        
        return len(g)

进一步,可以把 nums 当作 g 数组(滚动变化),此时空间复杂度为 O(1)

python
class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        ng = 0 # 表示 g 数组的长度
        for x in nums:
            j = bisect_left(nums, x, 0, ng) # nums[0:ng] = g[:]

            if j == ng:
                nums[ng] = x
                ng += 1
            else:
                nums[j] = x
        
        return ng

bisect_left(nums, x, 0, 0) == 0,二分零区间的结果为 0。

变形:非严格递增

如果允许 LIS 中有相同元素,即非严格递增,那么 g 也是一个非严格递增序列(用小于的反证法证明)。这个时候,和 nums[i] 相同的 g[j] 就不用修改了,修改的是第一个 > nums[i]g[j]

python
nums
  = [1, 6, 7, 2, 2, 5, 2]
g = [1]
g = [1, 6]
g = [1, 6, 7]
g = [1, 2, 7]
g = [1, 2, 2]
g = [1, 2, 2, 5]
g = [1, 2, 2, 2]

代码

python
class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        ng = 0 # 表示 g 数组的长度
        for x in nums:
            j = bisect_right(nums, x, 0, ng) # nums[0:ng] = g[:]

            if j == ng:
                nums[ng] = x
                ng += 1
            else:
                nums[j] = x
        
        return ng

Released under the MIT License.