最长上升(下降/不下降)子序列LIS

本文介绍了如何使用动态规划解决最长上升子序列问题,提供了一个O(n²)和优化后的O(nlogn)算法模板,并通过样例解释了两种方法。同时涉及到了最长不上升子序列的求解,包括二分查找优化过程。

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

(广东工业大学 ACM寒假集训 专题二)
最长上升子序列
子序列:子序列是指一段序列中递增的一段,比如序列12 2 4 5 8 3 3,那么2 5 8 3就是这个序列的子序列,不一定要连续。
那么上升子序列就是递增的子序列。
有两种方法求这个子序列的最长长度,我们来看一个模板。
http://bailian.openjudge.cn/practice/2757/
题目:
对于给定的序列,求出最长上升子序列的长度
输入
输入的第一行是序列的长度N (1 <= N <= 1000)。第二行给出序列中的N个整数,这些整数的取值范围都在0到10000。
输出
最长上升子序列的长度。

样例输入
7
1 7 3 5 9 4 8
样例输出
4
答案的序列为:1 3 5 9或1 3 5 8
分析:
动态规划所研究的问题,一般有重复的子问题,而且对于当前状态无后效性,所以在设置状态的时候有些讲究(一本正经胡说八道,看不懂就往后看吧),在这道题我们设置**dp[i]**的意义为以i为终点时,这个序列的最长上升子序列的长度,终点是指最长上升子序列中的最后一个。
初始时默认dp[i]=1,因为数字本身长度为1。
我们先从头遍历数组,遍历一个数a[i]就要和前面的所有数a[j]( 0<j<i )作比较,如果a[i]比前面的数a[j]大,那么说明这个数字可以作为前面那个a[j]的下一个数(上升),可以构成序列后,就在原序列上+1即可,dp[i]=dp[j]+1。
因为每一个数字为终点时,最长上升子序列的长度都不一样,所以我们设置状态转移方程为

dp[i]=max(dp[j]+1,dp[i]);

最后我们得到的dp数组每个数都是不一样的,并不是最后一个最大,因此我们需要找到最大值输出。
参考代码:

#include<iostream>
#include<algorithm>
using namespace std;
int dp[1002];
int a[1002];
int main()
{
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
        scanf("%d",&a[i]);
        dp[i]=1;//初始化
    }
    for(int i=1;i<=n;i++){
        for(int j=1;j<i;j++){
            if(a[i]>a[j])
            dp[i]=max(dp[j]+1,dp[i]);
        }
    }
    cout<<*max_element(dp+1,dp+n+1);//找最大值
}

然而这种方法是O(n²)的,通常我们需要跟快的O(nlogn)的方法。

我们再来看一个模板题,题意第一问就是求序列的最长不上升子序列(和上升子序列一样的,只是换个符号)

[NOIP1999 普及组] 导弹拦截
题目描述
某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。某天,雷达捕捉到敌国的导弹来袭。由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。

输入导弹依次飞来的高度(雷达给出的高度数据是\le 50000≤50000的正整数),计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。

输入格式
1行,若干个整数(个数≤100000)

NOIP 原题数据规模不超过 2000。

输出格式
2行,每行一个整数,第一个数字表示这套系统最多能拦截多少导弹,第二个数字表示如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。

输入输出样例
在这里插入图片描述
这里我们需要二分查找优化找子序列的过程,不过这道题变成了最长不上升子序列。
这个时候我们设dp[i]的意义为:长度为i的子序列中末尾的最大值,i相当于子序列的长度。可以想象一下,这个序列的最后一位如果数字越大,那么构成更长的不上升子序列的长度期望会更长。
比如:6 4 2 1和6 4 3 2 两个序列,末尾越大越有可能会构成更长的子序列。

首先我们让dp[1]=a[1],接着开始往后枚举,我们每枚举一个就要判断dp中最后一位(dp[len])和a[i]的大小,如果a[i]更小,那么它可以作为这个子序列的下一项,(这里我们用len)也就是dp[++len]=a[i];如果a[i]大于dp[len],就要使用二分法找到dp序列中小于a[i]的第一位,然后替换,这里换成了比原来大的数。
比如:9 7 5 4 2 然后下一位是3,那么我们换掉比3小的第一位,也就是2,这时候,序列变成了9 7 5 4 3。

以此类推,遍历到最后,dp的长度len就是最长不上升子序列的长度。
第二问的话,可能不好理解,因为如果有两枚导弹,第一枚先来,第二枚后来,而第一枚的高度比第二枚低,那么必须要两台才行,如果后面还有一枚导弹比第二枚导弹还要高,不就要三台了吗……以此类推。

#include<iostream>
using namespace std;
int a[100010];
int p[100010];
int b[100010];
int main()
{
    int i=0,len1=0,ans;
    while(~scanf("%d",&a[++i]));//循环输入
    int n=i-1;//最后一次没有输入因此跳出循环,n=i-1
    b[0] = 100000 ;
    b[++len1]=a[1];
    for(i=2;i<=n;i++){
        if(a[i]<=b[len1]){
            b[++len1]=a[i];
        }
        else 
        {
            int l=0,r=len1,mid;
            while(l<=r){
                mid=l+(r-l)/2;
                if(a[i]<=b[mid]){
                    ans=mid;
                    l=mid+1;
                }
                else r=mid-1;
            }
            b[ans+1]=a[i];
        }
        /*for(int j=1;j<=len1;j++){
                cout<<b[j]<<' ';
            }
            cout<<endl;*/
    }
    cout<<len1<<endl;
    int sum=0;
    int m=0;
    while(m<n){
        int last=10000000;
        for(int j=1;j<=n;j++){
            if(a[j]>0 && a[j]<=last){
                m++;
                last=a[j];
                a[j]=-1;
            }
        }
        sum++;
    }
    cout<<sum;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值