【动态规划】最长递增子序列
最长递增子序列
简介
最长递增子序列(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\) 的递增子序列,即:
状态转移方程
每一个较大的元素结尾的子序列,都可以由以它前一个小于它的元素结尾的子序列转移而来。
这里,我们直接给出状态转移方程,当 \(j < i < n\ 且\ a_j < a_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
题目
分析
参考前面的算法分析。
代码实现
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
题目
分析
由于能嵌套的信封的长和宽必须严格递减,所以我们将原有的信封尺寸,先按照 \(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