type
status
date
slug
summary
tags
category
icon
password
300. 最长递增子序列 | 力扣 | LeetCode | 🟠
给你一个整数数组
nums
,找到其中最长严格递增子序列的长度。子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,
[3,6,2,7]
是数组 [0,3,1,6,2,2,7]
的子序列。示例 1:
示例 2:
示例 3:
提示:
1 <= nums.length <= 2500
104 <= nums[i] <= 104
进阶:
- 你能将算法的时间复杂度降低到
O(n log(n))
吗?
题目来源:力扣 300. 最长递增子序列。
注意「子序列」和「子串」这两个名词的区别,子串一定是连续的,而子序列不一定是连续的。下面先来设计动态规划算法解决这个问题。
一、动态规划解法
动态规划的核心设计思想是数学归纳法。
相信大家对数学归纳法都不陌生,高中就学过,而且思路很简单。比如我们想证明一个数学结论,那么我们先假设这个结论在
k < n
时成立,然后根据这个假设,想办法推导证明出 k = n
的时候此结论也成立。如果能够证明出来,那么就说明这个结论对于 k
等于任何数都成立。类似的,我们设计动态规划算法,不是需要一个 dp 数组吗?我们可以假设
dp[0...i-1]
都已经被算出来了,然后问自己:怎么通过这些结果算出 dp[i]
?直接拿最长递增子序列这个问题举例你就明白了。不过,首先要定义清楚 dp 数组的含义,即
dp[i]
的值到底代表着什么? 我们的定义是这样的:
dp[i]
表示以 nums[i]
这个数结尾的最长递增子序列的长度。
根据这个定义,我们的最终结果(子序列的最大长度)应该是 dp 数组中的最大值。
如何设计算法逻辑进行状态转移,才能正确运行呢?这里需要使用数学归纳的思想:
假设我们已经知道了
dp[0..4]
的所有结果,我们如何通过这些已知结果推出 dp[5]
呢?根据刚才我们对
dp
数组的定义,现在想求 dp[5]
的值,也就是想求以 nums[5]
为结尾的最长递增子序列。nums[5] = 3
,既然是递增子序列,我们只要找到前面那些结尾比 3 小的子序列,然后把 3 接到这些子序列末尾,就可以形成一个新的递增子序列,而且这个新的子序列长度加一。nums[5]
前面有哪些元素小于 nums[5]
?这个好算,用 for 循环比较一波就能把这些元素找出来。以这些元素为结尾的最长递增子序列的长度是多少?回顾一下我们对
dp
数组的定义,它记录的正是以每个元素为末尾的最长递增子序列的长度。以我们举的例子来说,
nums[0]
和 nums[4]
都是小于 nums[5]
的,然后对比 dp[0]
和 dp[4]
的值,我们让 nums[5]
和更长的递增子序列结合,得出 dp[5] = 3
:这样的d[4] d[3]都能算出来了
完整实现
至此,这道题就解决了,时间复杂度 。总结一下如何找到动态规划的状态转移关系:
1、明确
dp
数组的定义。这一步对于任何动态规划问题都很重要,如果不得当或者不够清晰,会阻碍之后的步骤。2、根据
dp
数组的定义,运用数学归纳法的思想,假设 dp[0...i-1]
都已知,想办法求出 dp[i]
,一旦这一步完成,整个题目基本就解决了。但如果无法完成这一步,很可能就是
dp
数组的定义不够恰当,需要重新定义 dp
数组的含义;或者可能是 dp
数组存储的信息还不够,不足以推出下一步的答案,需要把 dp
数组扩大成二维数组甚至三维数组。目前的解法是标准的动态规划,但对最长递增子序列问题来说,这个解法不是最优的,可能无法通过所有测试用例了,下面讲讲更高效的解法。
三、拓展到二维
354. 俄罗斯套娃信封问题 | 力扣 | LeetCode | 🔴
给你一个二维整数数组
envelopes
,其中 envelopes[i] = [wi, hi]
,表示第 i
个信封的宽度和高度。当另一个信封的宽度和高度都比这个信封大的时候,这个信封就可以放进另一个信封里,如同俄罗斯套娃一样。
请计算 最多能有多少个 信封能组成一组“俄罗斯套娃”信封(即可以把一个信封放到另一个信封里面)。
注意:不允许旋转信封。
示例 1:
示例 2:
提示:
1 <= envelopes.length <= 105
envelopes[i].length == 2
1 <= wi, hi <= 105
题目来源:力扣 354. 俄罗斯套娃信封问题。
这道题的解法比较巧妙:
先对宽度
w
进行升序排序,如果遇到 w
相同的情况,则按照高度 h
降序排序;之后把所有的 h
作为一个数组,在这个数组上计算 LIS 的长度就是答案。画个图理解一下,先对这些数对进行排序:

然后在
h
上寻找最长递增子序列,这个子序列就是最优的嵌套方案:

那么为什么这样就可以找到可以互相嵌套的信封序列呢?稍微思考一下就明白了:
首先,对宽度
w
从小到大排序,确保了 w
这个维度可以互相嵌套,所以我们只需要专注高度 h
这个维度能够互相嵌套即可。其次,两个
w
相同的信封不能相互包含,所以对于宽度 w
相同的信封,对高度 h
进行降序排序,保证二维 LIS 中不存在多个 w
相同的信封(因为题目说了长宽相同也无法嵌套)。下面看解法代码: