LeetCode-300 最长递增子序列
此问题第二种贪心解法可能不容易理解,可以结合图片和代码一起,会好理解很多!有问题可以评论哈!🎉
最长递增子序列
题目描述
给你一个整数数组 nums
,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7]
是数组 [0,3,1,6,2,2,7]
的子序列。
数据样例
示例 1:
1 | 输入:nums = [10,9,2,5,3,7,101,18] |
示例 2:
1 | 输入:nums = [0,1,0,3,2,3] |
示例 3:
1 | 输入:nums = [7,7,7,7,7,7,7] |
提示:
- $1 \leq nums.length \leq 2500$
- $-10 ^ 4 \leq nums[i] \leq 10 ^ 4$
进阶:
你能将算法的时间复杂度降低到 $O(nlog(n))$ 吗?
动态规划 - $O(N ^ 2)$
动态规划
状态表示:$f[i]$ 表示以 $i$ 结尾的最长严格单调递增子序列的长度。
状态计算:
我们考虑前面所有可以将 $i$ 拼接到后面的子序列,我们知道,为了保证子序列是严格单调递增的,那么:
$i$ 可以拼接到前面序列中比 $nums[i]$ 小的的子序列后面。
因此状态转移方程为:
$$
f[i] = max(f[i], f[j] + 1), nums[j] < nums[i]
$$
时间复杂度 - $O(N ^ 2)$
每次状态计算需要遍历前面所有状态,因此时间复杂度是:$O(N ^ 2)$
代码
1 | func lengthOfLIS(nums []int) int { |
1 | class Solution { |
贪心,二分 - $O(N * log(N))$
贪心思想
考虑当前第 $i$ 个字符的前面所有可能的严格递增子序列,按照 序列长度 进行分类,我们以第一个样例为例来说明这个过程:
PS:每个序列长度里面的序列我们只选择 末尾元素最小 的序列。
[10,9,2,5,3,7,101,18]
,假设 $i = 4, nums[i] = 3$,此时划分结果如下:
- 长度为 $1$:
2
- 可能的序列有:10, 9, 2, 5, 3
,末尾元素最小的序列是2
- 长度为 $2$:
2 3
- 可能的序列有:2 5, 2 3
,末尾元素最小的序列是2 3
(因为 $3 < 5$) - 长度为 $3$: 不存在长度为 $3$ 的严格递增的子序列
此时我们总结一下上述 算法过程:
- 依次从左往右枚举序列
- 如果比前面 最长的序列 中的末尾元素 大,那么可以得到一个更长的序列
- 否则:当前元素替换掉前面序列中末尾元素比当前元素大的序列的末尾元素(贪心)
二分
按照上述算法过程,我们可以利用数组来模拟这个过程(参考下面图理解):
f[i]
表示长度为i
的严格递增的子序列 末尾元素最小值len
标记当前最长严格递增子序列的长度- 依次从左至右枚举序列:
- 如果
nums[i] > f[len]
,那么我们得到一个更长的子序列:f[++len] = nums[i]
- 否则,我们从
f[1], f[2], ..., f[len]
寻找第一个大于等于nums[i]
的序列长度,假设为f[x]
,那么贪心考虑f[x] = nums[i]
- 如果
f[]
数组是递增的,可以利用二分寻找大于等于 nums[i]
的 f[x]
- 每次
nums[i] > f[len]
的时候添加一个更大的元素,此时f
是递增的 - 每次替换掉前面某个序列末尾元素后,不影响单调性
PS:这里如果不理解,可以评论或者私信呀!
时间复杂度 - $O(N * log(N))$
- 枚举序列,复杂度为 $O(N)$
- 每次二分找到前面大于等于当前
nums[i]
的位置:$O(log(N))$
因此整体时间复杂度为:$O(N * log(N))$
代码
1 | func lengthOfLIS(nums []int) int { |
1 | class Solution { |
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Cat's Blog!
评论