动态规划
动态规划 、优化(以贪心和二分作为子过程) - 最长上升子序列 - 力扣(LeetCode)
为了从一个较短的上升子序列得到一个较长的上升子序列,主要关心这个较短的上升子序列结尾的元素。由于要保证子序列的相对顺序,在程序读到一个新的数的时候,如果比已经得到的子序列的最后一个数还大,那么就可以放在这个子序列的最后,形成一个更长的子序列;
假设dp[i]表示以元素nums[i]为最大值的子序列长度,dp[i]之前的元素值已知,那应该怎么计算dp[i]呢?
对nums[i]之前的(下标小于i)的每一个小于nums[i]的元素(只有这些元素才可以和nums[i]组成上升序列),假设这些元素集合为A,那么:
dp[i] = max( f[x] for x in A ) + 1;
这是一个套娃的过程,这么理解:nums[i]之前的那些比它要小的元素,都可以和它组成上升序列,而我们肯定想找能够组成的序列长度更长的那个。看代码注释:
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n = nums.size(), res = 0;
vector<int> dp(n, 1);//以nums[i]为最大值的子序列长度
for(int i = 0; i < n; ++i){
//查找nums[i]前面所有比nums[i]小的元素的最大子序列长度
for(int j = 0; j < i; ++j){
if(nums[j] < nums[i]) dp[i] = max(dp[i], 1+dp[j]);
}
res = max(res, dp[i]);
}
return res;
}
};
代码的复杂度,双层循环显然为O(n^2),外层循环显然不可避免,那内层循环可以优化吗?
贪心 + 二分
原来是使用贪心,居然没想到,类似于维护一个单调栈。
我们需要的是尽可能长的上升序列,那么对于同样长度的上升序列,肯定是尾元素越小越好。
方法一,例子中的nums前三个元素为10, 9, 2,对应的上升序列长度都是1,但是 2 应该被保留,10, 9则可以舍弃掉,因为后面的元素如果可以接在10/9后面形成上升序列,那一定也可以接在2后面,反之则未必。
所以我们维护一个单调栈f,f[i]表示长度为i的最长上升子序列的末尾元素的最小值,还是上面的例子:
10进来的时候,以10结尾的最长上升序列长度为1,那么f[1] = 10;
9进来的时候,以9结尾的最长上升序列长度也为1,长度都是1,但9 < 10,更新f[1] = 9;
2进来的时候长度也为1,2 < 9,更新f[1] = 2
…
可以去这儿看ppt:
动态规划 、优化(以贪心和二分作为子过程) - 最长上升子序列 - 力扣(LeetCode)
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n = nums.size();
if(n < 2) return n;
vector<int> f;//f[i]最大子序列长度为i的对应nums元素最小值
f.push_back(nums[0]);//先push第一个
for(int i = 1; i < n; ++i){
if(nums[i] > f.back()) f.push_back(nums[i]);
else{
auto it = lower_bound(f.begin(), f.end(), nums[i]);
*it = nums[i];
}
}
return f.size();
}
};
这个思路真的很厉害。
上面的代码可以简洁一点:
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
vector<int> f;//f[i]最大子序列长度为i的对应nums元素最小值
for(int i = 0; i < nums.size(); ++i){
if(f.empty() || nums[i] > f.back()) f.push_back(nums[i]);
else *lower_bound(f.begin(), f.end(), nums[i]) = nums[i];
}
return f.size();
}
};