最小上升子序列2

本文介绍了两种求解最小上升子序列问题的算法实现,一种复杂度为O(n^2),另一种通过优化达到O(n log n)。这两种方法分别使用状态转移方程和lower_bound函数进行计算。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最小上升子序列是动态规划的一个小分支,这一类的题目一般有两种解法,我们先看杭电的一个例题

Problem Description

某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统.但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能超过前一发的高度.某天,雷达捕捉到敌国的导弹来袭.由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹.
怎么办呢?多搞几套系统呗!你说说倒蛮容易,成本呢?成本是个大问题啊.所以俺就到这里来求救了,请帮助计算一下最少需要多少套拦截系统.
 


Input
输入若干组数据.每组数据包括:导弹总个数(正整数),导弹依此飞来的高度(雷达给出的高度数据是不大于30000的正整数,用空格分隔)
 


Output
对应每组数据输出拦截所有导弹最少要配备多少套这种导弹拦截系统.
 


Sample Input
8 389 207 155 300 299 170 158 65
 


Sample Output
2


该题的意思是给你一串数,让你求出最小上升子序列的个数。
一 . 复杂度为n*2的算法:这个方式的重点是推出状态转移方程


#include<stdio.h>
#include<algorithm>
#include<string.h>
using namespace std;
#define MAXN 30010
int main()
{
int n;
int dp[MAXN];//do[i]表示以i为结尾的最小上升子序列的长度//
int a[MAXN];//储存输入的数据// 
while(~scanf("%d",&n))
{
int ans = 0;
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
}
for(int i = 1;i <= n;i++)
{
dp[i]=1;//最低长度为1// 
for(int j = 1;j <= i;j++)
{
if(a[i] > a[j])//重点和难点都是这一步// 
dp[i]=max(dp[i],dp[j]+1);
}
ans=max(dp[i],ans);
}
printf("%d\n",ans);
}
return 0;
}


二.复杂度为n*log n 的方法,该方法与第一种方法相比较优化了第二个for循环。在看代码之前先介绍一个c++里面函数low_bound
格式为 lower_bound(begin,end,变量)意思是找到某个数组中第一个不小于该变量的元素的位置。
例如lower_bound(dp,dp+n,INF)表示dp数组中第一个不小于无穷大的元素的位置。
下面我们看一下代码:
#include <cstdio>  
#include <algorithm>  
#define INF 0x3f3f3f  
using namespace std;  
int dp[30010],a[30010];  
int main()  
{  
    int n,i,j;  
    while(scanf("%d",&n)!=EOF)  
    {  
        for(i=0;i<n;++i)  
        {  
            scanf("%d",&a[i]);  
            dp[i]=INF;  
        }  
        for(i=0;i<n;++i)  
            *lower_bound(dp,dp+n,a[i])=a[i];  
        printf("%d\n",lower_bound(dp,dp+n,INF)-dp);
    }  
    return 0;  
}   
<think>我们正在处理一个关于最长上升子序列(LongestIncreasingSubsequence,LIS)的问题,用户特别提到了动态规划求解最优解。根据引用[3]:最长上升子序列是指给定序列的一个最长的、数值从低到高排列的子序列。例如,序列[2,1,5,3,6,4,6,3]的最长上升子序列为[1,3,4,6]和[2,3,4,6],长度均为4。动态规划是解决最长上升子序列问题的经典方法。下面我们将按照动态规划的步骤来详细解释:步骤1:定义状态设dp[i]表示以第i个元素(下标为i)结尾的最长上升子序列的长度。步骤2:状态转移方程对于每一个位置i,我们遍历j从0到i-1:如果nums[i]>nums[j],那么说明nums[i]可以接在以nums[j]结尾的上升子序列后面,形成一个更长的上升子序列。因此,dp[i]=max(dp[i],dp[j]+1)(j从0到i-1,且nums[i]>nums[j])如果对于所有的j,nums[i]都不大于nums[j],那么dp[i]=1(即只有自身)。步骤3:初始化每个位置的dp[i]至少为1(因为至少可以包含自身)。步骤4:计算最优值我们遍历整个数组,计算出每个位置的dp[i],然后取dp数组中的最大值,即为最长上升子序列的长度。步骤5:构造最优解(如果需要输出子序列)我们可以通过记录前驱节点来还原最长上升子序列。具体地,在更新dp[i]的时候,记录是从哪个j转移过来的(即令pre[i]=j),最后从最大的dp[i]的位置开始回溯即可。但是注意:有时候最长上升子序列可能不止一个,所以构造最优解时可能需要考虑所有可能。下面我们用C++代码实现动态规划求解最长上升子序列的长度(如果需要输出序列,则再添加构造部分)。注意:这个算法的时间复杂度为$O(n^2)$。另外,还有一种利用贪心+二分查找的方法,可以将时间复杂度优化到$O(n\logn)$,但用户目前要求的是动态规划,所以我们先提供动态规划的解法。代码实现(C++):```cpp#include<iostream>#include<vector>#include<algorithm>usingnamespacestd;intlengthOfLIS(vector<int>&nums){intn=nums.size();if(n==0)return0;vector<int>dp(n,1);//初始化dp数组,每个元素至少为1intmaxLength=1;for(inti=1;i<n;i++){for(intj=0;j<i;j++){if(nums[i]>nums[j]){dp[i]=max(dp[i],dp[j]+1);}}maxLength=max(maxLength,dp[i]);}returnmaxLength;}intmain(){vector<int>nums={2,1,5,3,6,4,6,3};cout<<"最长上升子序列的长度为:"<<lengthOfLIS(nums)<<endl;return0;}```上述代码会输出4,因为最长上升子序列的长度为4。如果需要输出其中一个最长上升子序列,我们可以记录每个位置的前驱:```cppvector<int>getLIS(vector<int>&nums){intn=nums.size();if(n==0)return{};vector<int>dp(n,1);vector<int>pre(n,-1);//记录前驱,-1表示没有前驱intmaxLength=1;intendIndex=0;//记录最长上升子序列的最后一个元素的下标for(inti=1;i<n;i++){for(intj=0;j<i;j++){if(nums[i]>nums[j]){if(dp[j]+1>dp[i]){dp[i]=dp[j]+1;pre[i]=j;//记录从j转移过来}}}if(dp[i]>maxLength){maxLength=dp[i];endIndex=i;}}//回溯构造序列vector<int>lis;while(endIndex!=-1){lis.push_back(nums[endIndex]);endIndex=pre[endIndex];}reverse(lis.begin(),lis.end());returnlis;}```注意:如果有多个最长上升子序列,上述方法只能输出其中一个(具体取决于内层循环中j的顺序,我们取的是最后一个更新dp[i]的j,但实际上可能有多个j满足条件,这里我们只记录最后一个使得dp[i]更新的j,所以输出一个序列)。如果用户需要输出所有最长上升子序列,则需要在动态规划的基础上,使用额外的结构(例如每个节点记录所有可能的前驱)进行回溯,这样会比较复杂且耗时,通常我们只要求输出长度或一个序列。总结:动态规划求解最长上升子序列的时间复杂度为$O(n^2)$,空间复杂度为$O(n)$。相关问题:1.动态规划求解最长上升子序列的时间复杂度是多少?有没有更优的算法?2.如何输出所有的最长上升子序列?3.最长上升子序列问题有哪些变种?4.动态规划求解最长上升子序列和贪心+二分查找的方法有何区别?</think>### 最长上升子序列(LIS)的动态规划解法 最长上升子序列(Longest Increasing Subsequence)是指在一个序列中找到一个最长的子序列,使得这个子序列的元素严格递增。动态规划是解决该问题的经典方法,以下是详细步骤: #### 算法步骤 1. **定义状态** 设 $dp[i]$ 表示以第 $i$ 个元素结尾的最长上升子序列的长度。 2. **状态转移方程** 对于每个位置 $i$,遍历 $j \in [0, i-1]$: $$ dp[i] = \max(dp[i], \quad dp[j] + 1) \quad \text{其中} \quad nums[j] < nums[i] $$ 若所有 $nums[j] \geq nums[i]$,则 $dp[i] = 1$(仅包含自身)。 3. **初始化** $dp$ 数组初始值全为 $1$(每个元素自身构成长度为 $1$ 的子序列)。 4. **计算最优值** 遍历完成后,$dp$ 数组的最大值即为最长上升子序列的长度。 5. **构造最优解**(可选) 通过反向追踪 $dp$ 数组还原具体子序列: - 从 $dp$ 最大值对应的位置开始 - 向前查找满足 $nums[j] < nums[i]$ 且 $dp[j] = dp[i] - 1$ 的元素 #### 时间复杂度 - 时间复杂度:$O(n^2)$(嵌套遍历) - 空间复杂度:$O(n)$(存储 $dp$ 数组) #### C++ 代码实现 ```cpp #include <iostream> #include <vector> #include <algorithm> using namespace std; int lengthOfLIS(vector<int>& nums) { int n = nums.size(); vector<int> dp(n, 1); // 初始化dp数组 int maxLen = 1; for (int i = 1; i < n; i++) { for (int j = 0; j < i; j++) { if (nums[i] > nums[j]) { dp[i] = max(dp[i], dp[j] + 1); } } maxLen = max(maxLen, dp[i]); } return maxLen; } int main() { vector<int> nums = {10, 9, 2, 5, 3, 7, 101, 18}; cout << "最长上升子序列长度: " << lengthOfLIS(nums) << endl; // 输出4 (子序列[2,5,7,101]) return 0; } ``` #### 优化方法(进阶) 对于大型序列,可优化至 $O(n \log n)$: 1. **贪心 + 二分查找** 维护一个有序数组 `tail`,`tail[i]` 表示长度为 $i+1$ 的上升子序列最小末尾元素。遍历时用二分查找替换元素。 #### 应用场景 - DNA序列比对[^3] - 金融分析(股票价格趋势预测) - 路径规划(机器人避障轨迹) - 数据压缩(删除冗余元素) --- ### 相关问题 1. **动态规划求解最长上升子序列的时间复杂度如何优化到 $O(n \log n)$?** 2. **如何修改算法以输出所有可能的最长上升子序列?** 3. **最长上升子序列与最长公共子序列(LCS)有何异同?** 4. **动态规划在求解最长上升子序列时有哪些局限性?** [^1]: 动态规划法通过分解子问题避免重复计算 [^2]: 动态规划的核心是状态定义和转移方程 [^3]: 子序列需保持原序列的先后次序 [^4]: 最长公共子序列的求解思路可类比参考
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值