300. 最长上升子序列
难度中等
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:
- 可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
- 你算法的时间复杂度应该为 O(n2) 。
进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?
分析
首先,需要对「子序列」和「子串」这两个概念进行区分;
- 子序列(subsequence)
子序列并不要求连续,例如:序列 [4, 6, 5]
是 [1, 2, 4, 3, 7, 6, 5]
的一个子序列。
- 子串(substring、subarray)
子串一定是连续的,例如:「力扣」第 3 题:“无重复字符的最长子串”,「力扣」第 53 题:“最大子序和”。
其次,题目中的「上升」的意思是「严格上升」,[1, 2, 2, 3]
都不能算作「上升子序列」;
第三,子序列中元素的相对顺序很重要。
它们必须保持在原始数组中的相对顺序。如果把这个限制去掉,将原始数组去重以后,元素的个数即为所求。
方法一:暴力解法
- 使用「回溯搜索算法」或者「位运算」的技巧,可以得到输入数组的所有子序列,时间复杂度为 O ( 2 N ) O(2^N) O(2N);
- 再对这些子串再依次判定是否为「严格上升」,时间复杂度 为O(N),所以总的时间复杂度为: O ( N 2 N ) O(N2^N) O(N2N)。
这道题是很经典的使用「动态规划」算法解决的问题。
「动态规划」的基本思想是:
从一个小问题出发(「动态」这个词的意思的意思),通过「状态转移」,并且逐步记录求解问题的过程(「规划」这个词的意思,就是「打表格」,以「以空间换时间」),逐步得到所求规模的问题的解。
对于这道题来说,就是一个数一个数地去考虑(区别于递归的写法,直接面对问题求解)。
方法二:动态规划
- 首先考虑题目问什么,就把什么定义成状态。
- 题目问最长上升子序列的长度,其实可以把「子序列的长度」定义成状态,但是发现「状态转移」不好做;
- 把「子序列的长度」定义成状态,事实上也可以,只是目前这样定义状态,没有定义得很清晰,具体做法在下文「方法三」;
- 为了从一个较短的上升子序列得到一个较长的上升子序列,我们主要关心这个较短的上升子序列结尾的元素。由于要保证子序列的相对顺序,在程序读到一个新的数的时候,如果比已经得到的子序列的最后一个数还大,那么就可以放在这个子序列的最后,形成一个更长的子序列;
于是我们可以这样定义状态:
第 1 步:定义状态
由于一个子序列一定会以一个数结尾,于是将状态定义成:dp[i]
表示以 nums[i]
结尾的「上升子序列」的长度。注意:这个定义中 nums[i]
必须被选取,且必须是这个子序列的最后一个元素。
第 2 步:考虑状态转移方程
- 遍历到
nums[i]
时,需要把下标i
之前的所有的数都看一遍; - 只要
nums[i]
严格大于在它位置之前的某个数,那么nums[i]
就可以接在这个数后面形成一个更长的上升子序列; - 因此,
dp[i]
就等于下标i
之前严格小于nums[i]
的状态值的最大者 +1。
语言描述:在下标 i
之前严格小于 nums[i]
的所有状态值中的最大者 + 1。
符号描述:
d p [ i ] = max 0 ≤ j < i , n u m s [ j ] < n u m s [ i ] d p [ j ] + 1 , dp[i] = \max_{0 \le j < i, nums[j] < nums[i]} {dp[j] + 1}, dp[i]=max0≤j<i,nums[j]<nums[i]dp[j]+1,
第 3 步:考虑初始化
dp[i] = 1
,1 个字符显然是长度为 1 的上升子序列。
第 4 步:考虑输出
- 这里要注意,不能返回最后一个状态值;
- 还是根据定义,最后一个状态值只是以
nums[len - 1]
结尾的「上升子序列」的长度; - 状态数组
dp
的最大值才是最后要输出的值。
max