【动态规划】——最长递增子序列问题

今天来总结一下动态规划问题中一类经典问题:最长递增子序列

经典题型:

LeetCode 300.Longest Increasing Subsequence

Given an integer array nums, return the length of the longest strictly increasing subsequence.

A subsequence is a sequence that can be derived from an array by deleting some or no elements without changing the order of the remaining elements. For example, [3,6,2,7] is a subsequence of the array [0,3,1,6,2,2,7].

方法一:动态规划

数组dp[i]表示以第i个数字结尾的最长子序列长度。我们在计算dp[i]之前已经计算了[0:i1]的情况。所以我们可以得到状态转移方程为dp[i]=max0<j<i(dp[j]+1)

由于dp[j]表示以第j个数字结尾的最长子序列长度。所以上述状态转移方程如果成立,那么nums[i] 必然要大于nums[j],才能将nums[i] 放在nums[j]后面以形成更长的上升子序列。

最终答案就是dp数组中的最大值

class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n=nums.size();
vector<int> dp(n+1,1);
int maxNum=dp[0];
for(int i=0;i<n;i++){
for(int j=0;j<i;j++){
if(nums[i]>nums[j]){
dp[i]=max(dp[i],1+dp[j]);
}
}
maxNum=max(maxNum,dp[i]);
}
return maxNum;
}
};

时间复杂度:$O(n2)O(n)$.

方法二:基于贪心+二分查找的动态规划

方法一的时间复杂度为O(n2),在很多情况下这是不可接受的。我们可以利用贪心+二分查找来减少时间复杂度。

贪心的策略:我们想要递增子序列尽可能的长,所以加入dp[i]末尾的元素要尽可能的小。

我们可以将动态规划中定义的dp数组的状态值与状态交换,即:dp[i]表示长度为i+1LIS的末尾元素的最小值。

下面我们简单证明一下dp数组是递增的:即对于任意的i<jdp[i]<dp[j]

反证法:我们假设存在k<j,dp[k]>dp[j],根据上面的假设:dp[j]表示长度为j+1的最长递增子序列的末尾元素的最小值。那么我们在长度为j+1,末尾元素为dp[j]的子序列中删除后ji个元素。得到一个长度为i的子序列,其末尾元素为x一定小于dp[j].

dp[1],dp[2],dp[3],...,dp[k],...,x,(dp[i],...dp[j])

 

又由于dp[k]>dp[j],所以dp[k]>x。那么dp[k]就不是长度为k的最长递增子序列末尾元素的最小值。与题设矛盾。

算法流程:

设当前已求出的最长上升子序列的长度为len(初始时为1),从前往后遍历数组nums,在遍历到 nums[i] 时:

如果nums[i]>d[len] ,则直接加入到 dp数组末尾,并更新 len=len+1

否则,在 dp数组中二分查找,找到第一个比 nums[i] 小的数dp[k],并更新 dp[k+1]=nums[i]

class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n=nums.size();
vector<int> dp;
dp.push_back(nums[0]);
for(int i=0;i<n;i++){
if(nums[i]>dp.back()){
dp.push_back(nums[i]);
}
else{
auto ptr=lower_bound(dp.begin(),dp.end(),nums[i]);
*ptr=nums[i];
}
}
return dp.size();
}
};

变式:二维的最长递增子序列问题

这一类问题可以通过固定一维来将其降维到一维。

LeetCode 354 俄罗斯套娃问题

You are given a 2D array of integers envelopes where envelopes[i] = [wi, hi] represents the width and the height of an envelope.

One envelope can fit into another if and only if both the width and height of one envelope are greater than the other envelope's width and height.

Return the maximum number of envelopes you can Russian doll (i.e., put one inside the other).

Note: You cannot rotate an envelope.

 

Example 1:

Input: envelopes = [[5,4],[6,4],[6,7],[2,3]]
Output: 3
Explanation: The maximum number of envelopes you can Russian doll is 3 ([2,3] => [5,4] => [6,7]).


Example 2:

Input: envelopes = [[1,1],[1,1],[1,1]]
Output: 1
 

Constraints:

1 <= envelopes.length <= 105
envelopes[i].length == 2
1 <= wi, hi <= 105

 

这题我们首先要将所给数组排序,特别注意当wi==wj时,针对h的排序需要降序,因为如果h也是升序排列的话,不能确保满足题意(题目要求是w和h都比下一个小才能装入)。举个例子:

[(w, h)] = [(1, 1), (1, 2), (1, 3), (1, 4)][(w,h)]=[(1,1),(1,2),(1,3),(1,4)],由于这些信封的 w值都相同,不存在一个信封可以装下另一个信封,那么我们只能在其中选择 1个信封。然而如果我们完全忽略 w维度,剩下的 h维度为 [1, 2, 3, 4],这是一个严格递增的序列,那么我们就可以选择所有的4个信封了,这就产生了错误。

如果我们按降序排列,在w相同的情况下,h维度不可能形成长度超过1的最长递增子序列,从而避免了错误。

这题由于数据量太大,使用方法一不适合。故在这里只贴方法二的代码:

class Solution {
public:
int maxEnvelopes(vector<vector<int>>& envelopes) {
int n=envelopes.size();
sort(envelopes.begin(),envelopes.end(),
[](const vector<int>& v1,const vector<int>& v2){
if(v1[0]==v2[0]){
return v1[1]>v2[1];
}
return v1[0]<v2[0];
});
vector<int> dp{envelopes[0][1]};
for(int i=1;i<n;i++){
if(envelopes[i][1]>dp.back()){
dp.push_back(envelopes[i][1]);
}
else{
auto it=lower_bound(dp.begin(),dp.end(),
envelopes[i][1]);
*it=envelopes[i][1];
}
}
return dp.size();
}
};

 


__EOF__

本文作者天涯海角寻天涯
本文链接https://www.cnblogs.com/yjx-7355608/p/15948037.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   天涯海角寻天涯  阅读(65)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示