【C/C++】300. 最长递增子序列(经典题目)
我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情
题目链接:300. 最长递增子序列
题目描述
给你一个整数数组 nums
,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7]
是数组 [0,3,1,6,2,2,7]
的子序列。
提示:
1 <= nums.length <= 2500
-10^4 <= nums[i] <= 10^4
进阶:
- 你能将算法的时间复杂度降低到
O(n log(n))
吗?
示例 1:
输入: nums = [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例 2:
输入: nums = [0,1,0,3,2,3]
输出: 4
示例 3:
输入: nums = [7,7,7,7,7,7,7]
输出: 1
整理题意
题目给定一个整数数组 nums
,找到 nums
中 最长严格递增子序列 的长度。
子序列不要求连续,子数组才要求连续。
进阶要求时间复杂度为:O(nlogn)O(n \log n)O(nlogn)。
解题思路分析
动态规划
令 dp[i]
表示为以数字 nums[i]
结尾(包括 nums[i]
)的最长上升子序列的长度。那么我们从头到尾遍历数组,对应 nums[i]
来说,我们仅需遍历 [0, i)
(左闭右开区间)中所有整数,选取小于 nums[i]
的整数来跟新 dp[i]
的长度即可,遍历过程中维护 dp[i]
的最大值即可。
贪心+二分
贪心思维: 我们要使上升子序列尽可能的长,因此我们希望每次在上升子序列最后加上的那个数尽可能的小。所以 定义数组 dp[i]
表示长度为 i
的最长上升子序列的末尾元素的最小值。
贪心的关键在于: 定义数组 dp[i]
表示长度为 i
的最长上升子序列的末尾元素的最小值 ,即在数组 [1,2,3,4,5,6]
中长度为 3
的上升子序列可以为 [1,2,3]
也可以为 [2,3,4]
等等但是dp[3]=3
,即子序列末尾元素最小为 3
。
这是因为在上述数组中长度为
3
的上升子序列为[1,2,3]
的利用价值大于其他长度为3
的上升子序列,也就是 给后面留更多的空间。
具体实现
动态规划
- 令
dp[i]
表示为以数字nums[i]
结尾(包括nums[i]
)的最长上升子序列的长度; - 第一层循环从左到右遍历数组中的整数,对于每个整数寻找以其结尾的最长上升子序列的长度;
- 第二层循环遍历
[0, i)
(左闭右开区间)中所有整数,如果nums[j] < nums[i]
那么我们就跟新dp[i] = max(dp[i], dp[j] + 1)
; - 遍历的过程中用
ans
记录dp[i]
的最大值,最后返回ans
即可。
贪心+二分
- 定义数组
dp[i]
表示长度为i
的最长上升子序列的末尾元素的最小值; - 遍历数组
nums[i]
,对于每个nums[i]
来说,如果nums[i]
大于dp
数组最后一个数,那么直接让nums[i]
接在dp
数组后面即可,保证dp
数组为单调递增数组。 - 如果
nums[i]
小于dp
数组最后一个数,那么就在dp
数组中二分查找第一个大于等于nums[i]
的整数位置,nums[i]
代替其的位置。(因为dp
数组为单调递增数组,所以可以使用二分查找,又因为nums[i]
大于dp
数组最后一个数,所以一定能在dp
数组中找到第一个大于等于nums[i]
的整数位置。)
代替其位置就是为了给后面更多的空间,可以形象的比喻为:一个新员工和一个老员工价值相当,老员工就可以走了,因为新员工被榨取的剩余空间更多。
- 最后
dp
数组的长度即为nums
中 最长严格递增子序列 的长度。
复杂度分析
- 时间复杂度:
- 动态规划的时间复杂度为 O(n2)O(n^2)O(n2),其中 nnn 为数组
nums
的长度。动态规划的状态数为 nnn,计算状态dp[i]
时,需要 O(n)O(n)O(n) 的时间遍历dp
数组[0, i - 1]
的所有状态,所以总时间复杂度为 O(n2)O(n^2)O(n2)。 - 贪心+二分的时间复杂度为 O(nlogn)O(n\log n)O(nlogn)。数组
nums
的长度为 nnn,我们依次用数组中的元素去更新dp
数组,而更新dp
数组时需要进行 O(logn)O(\log n)O(logn) 的二分搜索,所以总时间复杂度为 O(nlogn)O(n\log n)O(nlogn)。
- 动态规划的时间复杂度为 O(n2)O(n^2)O(n2),其中 nnn 为数组
- 空间复杂度:两种方法都需要 O(n)O(n)O(n) 的空间复杂度,需要额外使用长度为 nnn 的
dp
数组。
代码实现
动态规划
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n = nums.size();
int dp[n], ans = 0;
memset(dp, 0, sizeof(dp));
for(int i = 0; i < n; i++){
dp[i] = 1;
for(int j = 0; j < i; j++){
if(nums[j] < nums[i]) dp[i] = max(dp[i], dp[j] + 1);
}
ans = max(ans, dp[i]);
}
return ans;
}
};
贪心+二分
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
vector<int> ans; ans.clear();
for(int &num : nums){
if(ans.size() == 0 || ans.back() < num){
ans.emplace_back(num);
}
else{
int pos = lower_bound(ans.begin(), ans.end(), num) - ans.begin();
ans[pos] = num;
}
}
return ans.size();
}
};
总结
- 该题是较为经典的寻找 最长递增子序列 的题目,是作为很多题目衍生的母题,也就是在此题基础上进行进一步扩展的。
- 动态规划的思想较为容易理解,但在代码实现上时间复杂度较差;而对于 贪心+二分搜索 的方法需要掌握,因为其在同样空间复杂度的情况下时间复杂度上是更优的,在对于其他衍生题目来说也是更有优势的。
- 测试结果:
通过对比两种方法的测试结果,可以看出 贪心+二分 的时间复杂度明显优于动态规划的方法。
结束语
聪明的人很多,靠谱的人却难得。一个靠谱的人,一定是负责任、有担当的人,也是坚守底线、有原则的人。许下承诺就尽力去实现,答应的事情绝不含糊,愿我们都能成为靠谱的人。新的一天,加油!
转载自:https://juejin.cn/post/7140550401430028302