Leetcode300. 最长上升子序列
经典 DP 问题之一 ——LIS。
描述
300. 最长上升子序列
Difficulty: 中等
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
1 | 输入: [10,9,2,5,3,7,101,18] |
说明:
- 可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
- 你算法的时间复杂度应该为 $O (n^2)$ 。
进阶:你能将算法的时间复杂度降低到 $O (nlogn)$ 吗?
思路 & 题解
经典 LIS
思路
对于所有的 DP 问题,经典四步走:
定义状态
由于一个子序列一定会以一个数结尾,于是将状态定义成:
dp [i]
表示以nums [i]
结尾的「上升子序列」的长度。注意:这个定义中`nums [i]
必须被选取,且必须是这个子序列的最后一个元素。考虑状态转移方程
遍历到
nums [i]
时,需要把下标i
之前的所有的数都看一遍;
只要nums [i]
严格大于在它位置之前的某个数,那么nums [i]
就可以接在这个数后面形成一个更长的上升子序列;
因此,dp [i]
就等于下标 i 之前严格小于nums [i]
的状态值的最大者 +1。
因此我们可以写出下面的状态转移方程:考虑初始化
dp [i] = 1,每个字符都至少构成长度为 1 的子序列
输出
状态数组 dp 的最大值才是最后要输出的值。
状态压缩
遍历到一个新数的时候,之前所有的状态值都得保留,因此无法压缩。
代码
1 |
|
二分查找 + 贪心
思路
题目中要找 $O (nlogn)$ 的做法,考虑到二分查找的复杂度为 $O (logn)$,所以可以考虑在第二层循环做文章,改成二分查找。
在这里,我们的出发点基于一个贪心的做法:结尾的数尽量小,遍历后接的数就有更大的可能性构成更长的序列。
我们可以记录长度固定的状况下,结尾最小的元素数值。
定义状态
tail [i]
表示长度为i+1
的所有上升子序列的结尾最小值。tail [0]
表示长度为 1 的所有上升子序列中,结尾最小的那个元素的数值,以题目中的示例为例 [10, 9, 2, 5, 3, 7, 101, 18] 中,容易发现长度为 2 的所有上升子序列中,结尾最小的是子序列 [2, 3] ,因此 tail [1] = 3
下标和长度有一个 1 的偏差;状态转移方程
这里可以证明
tail
严格上升。证明:对于任意下表 $0 \leq i < j < len$,都有 $tail [i]<tail [j]$
假设对于任意 $i
=tail [j]` 对于
tail [i]
,对应一个上升子序列 $[a_0,a_1,…,a_i]$,$tail [i]=a_i$对于
tail [j]
,对应一个上升子序列 $[b_0,b_1,…,b_j]$,$tail [j]=b_j$由于
tail [i]>=tail [j]
,$a_i>b_j$。而在 $[b_0,b_1,…,b_j]$ 中,$b_ib_i$,则上升子序列 $[b_0,b_1,…,b_j]$ 是一个长度为 i+1
但结尾更小的数组,与假设矛盾,命题得证。根据命题,我们维护
tail
即可直接取得最长上升子序列的长度。
算法
1、设置一个数组 tail,初始时为空;
注意:数组 tail
虽然是有序数组,但它不是问题中的「最长上升子序列」(下文还会强调),不能命名为 LIS。有序数组 tail 只是用于求解 LIS 问题的辅助数组。
2、在遍历数组 nums
的过程中,每来一个新数 num,如果这个数严格大于有序数组 tail
的最后一个元素,就把 num 放在有序数组 tail
的后面,否则进入第 3 点;
注意:这里的大于是「严格大于」,不包括等于的情况。
3、在有序数组 tail
中查找第 1 个等于大于 num 的那个数,试图让它变小;
如果有序数组 tail
中存在等于 num 的元素,什么都不做,因为以 num 结尾的最短的「上升子序列」已经存在;
如果有序数组 tail
中存在大于 num 的元素,找到第 1 个,让它变小,这样我们就找到了一个结尾更小的相同长度的上升子序列。
说明:我们再看一下数组 tail [i]
的定义:长度为 i + 1 的所有最长上升子序列的结尾的最小值。因此,在遍历的过程中,我们试图让一个大的值变小是合理的。
这一步可以认为是「贪心算法」,总是做出在当前看来最好的选择,当前「最好的选择」是:当前只让让第 1 个严格大于 nums [i]
的数变小,变成 nums [i]
,这一步操作是 “无后效性” 的。
由于是在有序数组中的操作,因此可以使用「二分查找算法」。
4、遍历新的数 num ,先尝试上述第 2 点,第 2 点行不通则执行第 3 点,直到遍历完整个数组 nums,最终有序数组 tail 的长度,就是所求的 “最长上升子序列” 的长度。
第 3 步:思考初始化dp [0] = nums [0]
,在只有 1 个元素的情况下,它当然是长度为 1 并且结尾最小的元素。
第 4 步:思考输出
数组 tail
的长度,上文其实也已经说了,还是依据定义,tail [i]
表示长度固定为 i + 1 的所有「上升子序列」的结尾元素中最小的那个,长度最多就是数组 tail
的长度。
第 5 步:思考状态压缩
无法压缩。
下面看一下这个算法在示例上的的执行流程,以加深体会,我在示例的数组后面加上了 4、8、6、12,依然是先让幻灯片动起来,看思想就好了。
代码
1 | public class Solution { |