【动态规划】最长递增子序列

最长递增子序列

简介

最长递增子序列(Longest Increasing Subsequence)是指找到一个给定序列的最长子序列的长度,使得子序列中的所有元素单调递增

例如,\(nums = [3,5,7,1,2,8]\)LIS\([3,5,7,8]\),长度为 \(4\)

分析

方法一: 转换为LCS

我们可以把 求最长递增子序列问题 转化为求 最长公共子序列 的问题。

例如,\(A = [3,5,7,1,2,8]\) 的最长递增子序列,那么可以将数组 \(A\) 排序,排序之后的数组为 \(B = [1, 2, 3, 5, 7, 8]\),只需求数组 \(A\) 与数组 \(B\) 的最长公共子序列即可。
数组 \(A\) 与数组 \(B\) 最长公共子序列就是 \([3,5,7,8]\),就是最长递增子序列。

参考:最长公共子序列

方法二:动态规划

设数组 \(nums = \{a_0,\ a_1,\ a_2,\ ...,\ a_{n-1}\}\)\(dp[i]\)表示以元素 \(a_i\) 结尾的最长递增子序列。

初始条件

每一个字符都是长度为 \(1\) 的递增子序列,即:

\[L[i] = 1 \]

状态转移方程

每一个较大的元素结尾的子序列,都可以由以它前一个小于它的元素结尾的子序列转移而来。

这里,我们直接给出状态转移方程,当 \(j < i < n\ 且\ a_j < a_i\) 时,有:

\[dp[i] = max(dp[j] + 1,\ dp[i]) \]

例如,\(A = [3,5,7,1,2,8]\),由于 \(a_1 > a_0\),所以 \(dp[1] = dp[0] + 1 = 2\)

代码实现

from typing import List

class Solution:
    @staticmethod
    def lengthOfLIS(nums: List[int]) -> int:
        n = len(nums)
        dp = [1] * n
        for i in range(1, n):
            for j in range(i):
                if nums[i] > nums[j]:
                    dp[i] = max(dp[j] + 1, dp[i])
        return max(dp)

方法三:二分法

\(tails[i]\) 表示长度为 \(i + 1\) 的递增子序列的末尾元素的最小值

也就是说,我们要保证 \(tails\) 数组中的每个位置的元素都是尽可能地小,同时,我们将 \(tail\) 全都初始化为 \(0\)

例如,假设 \(nums=[4, 5, 6, 3]\) ,那么:

  • \(i = 0\) 时,所有的递增子序列:\([4]\)\([5]\)\([6]\)\([3]\),此时 \(tails[0] = 3\)
  • \(i = 1\) 时, 所有的递增子序列:\([4, 5]\), \([5, 6]\), \([4, 6]\),此时 \(tails[1] = 5\)
  • \(i = 2\) 时, 所有的递增子序列:\([4, 5, 6]\),此时 \(tails[2] = 6\)
  • \(i = 3\) 时,不存在递增子序列。

容易看出,\(tails\) 是一个递增序列,所以,我们只需要遍历数组 \(nums\) ,对于每一个元素,通过二分查找,找到该元素可以被插入\(tails\) 数组中的位置即可,数组 \(nums\) 中的任意一个元素,在插入数组 \(tails[i]\) 中时,都有两种情况:

  • 要么,它能找到一个位置,并 替换 掉已有元素;
  • 或者,它大于当前 \(tails\) 中所有的元素,这时将其 追加 \(tails\) 已有元素的最后。

遍历完数组后,最终,\(tails\) 数组中不为零的元素个数,就是最长递增子序列的长度

例如,以 \(nums = [3, 9, 4, 7, 4, 12]\) 为例:

  • \(i = 0\) 时,在 \(tails\) 的位置 \(0\) 插入元素 \(3\)\(tails = [3, 0, 0, 0, 0, 0]\)
  • \(i = 1\) 时,在 \(tails\) 的位置 \(1\) 插入元素 \(9\)\(tails = [3, 9, 0, 0, 0, 0]\)
  • \(i = 2\) 时,在 \(tails\) 的位置 \(1\) 覆盖元素 \(4\)\(tails = [3, 4, 0, 0, 0, 0]\)
  • \(i = 3\) 时,在 \(tails\) 的位置 \(2\) 插入元素 \(7\)\(tails = [3, 4, 7, 0, 0, 0]\)
  • \(i = 4\) 时,在 \(tails\) 的位置 \(1\) 覆盖元素 \(4\)\(tails = [3, 4, 7, 0, 0, 0]\)
  • \(i = 5\) 时,在 \(tails\) 的位置 \(3\) 追加元素 \(12\)\(tails = [3, 4, 7, 12, 0, 0]\)

最终,\(tails\) 数组中不为零的元素为 \([3, 4, 7, 12]\),就是最长递增子序列的长度,所以,该数组的最长递增子序列长度为 \(4\)

代码实现

from typing import List

class Solution:
    @staticmethod
    def lengthOfLIS(nums: List[int]) -> int:
        """ 求最长递增子序列 """
        tails = [0] * len(nums)
        size = 0
        for num in nums:
            left, right = 0, size
            # 通过二分法,为num在tail数组中一个插入位置
            while left != right:
                mid = (left + right) // 2
                if tails[mid] < num:
                    left = mid + 1
                else:
                    right = mid
            tails[left] = num
            # 记录tail数组中不为零的元素的长度
            if left == size:
                size += 1

        return size

应用

应用1:Leetcode.300

题目

300. 最长递增子序列

分析

参考前面的算法分析。

代码实现

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        n = len(nums)
        dp = [1] * n
        for i in range(1, n):
            for j in range(i):
                if nums[i] > nums[j]:
                    dp[i] = max(dp[j] + 1, dp[i])
        return dp[n - 1]

应用2:Leetcode.354

题目

354. 俄罗斯套娃信封问题

分析

由于能嵌套的信封的长和宽必须严格递减,所以我们将原有的信封尺寸,先按照 \(w\) 升序排列,如果 \(w\) 相同的时候,将 \(h\) 降序排列,这样就得到一个排序的二维数组。

然后,再取排序后的\(h\)最长递增子序列,就是能嵌套的信封个数,即可得到答案。

代码实现

from typing import List

class Solution:
    def maxEnvelopes(self, envelopes: List[List[int]]) -> int:
        # 基于w升序,h降序排序
        envelopes = sorted(envelopes, key=lambda x: (x[0], -x[1]))
        # 寻找以h升序的最长子序列
        heights = [envelope[1] for envelope in envelopes]
        return self.lengthOfLIS(heights)

    @staticmethod
    def lengthOfLIS(nums: List[int]) -> int:
        """ 求最长递增子序列 """
        tails = [0] * len(nums)
        size = 0
        for num in nums:
            left, right = 0, size
            while left != right:
                mid = (left + right) // 2
                if tails[mid] < num:
                    left = mid + 1
                else:
                    right = mid
            tails[left] = num

            if left == size:
                size += 1

        return size
posted @ 2022-11-19 00:09  LARRY1024  阅读(231)  评论(0编辑  收藏  举报