DP之最长不下降子序列(LIS)——C++信息学奥赛一本通1259题解

1259:【例9.3】求最长不下降序列

时间限制: 1000 ms 内存限制: 65536 KB

提交数: 34970 通过数: 13471 Special Judge

【题目描述】

设有由n(1≤n≤200)个不相同的整数组成的数列,记为:b(1)b(2)……b(n)若存在i1<i2<i3<…<ie 且有b(i1)<=b(i2)<=…<=b(ie)则称为长度为e的不下降序列。程序要求,当原数列出之后,求出最长的不下降序列。

例如13,7,9,16,38,24,37,18,44,19,21,22,63,15。例中13,16,18,19,21,22,63就是一个长度为7的不下降序列,同时也有7 ,9,16,18,19,21,22,63组成的长度为8的不下降序列。

【输入】

第一行为n,第二行为用空格隔开的n个整数。

【输出】

第一行为输出最大个数max(形式见样例);

第二行为max个整数形成的不下降序列,答案可能不唯一,输出一种就可以了,本题进行特殊评测。

【输入样例】

14

13 7 9 16 38 24 37 18 44 19 21 22 63 15

【输出样例】

max=8

7 9 16 18 19 21 22 63

题解

本题是动态规划的经典问题。照例,我们先来分析一下题目中的阶段、状态、决策这三要素。刚学DP的同学可能难以发现,其实这类线性DP都是差不多的,我们就按照逆序的老方法来推一下吧。

首先我们先定义好dp数组的意义,一般定义为dp[i]表示第i个值为首位的最长不下降子序列的长度。注意题目中子序列与子串的区别,前者可以是不连续的,后者必须是连续的。同样的,从n-1开始枚举,这里我们用样例来演示(便于习惯,题目中的b数组写作a数组):

样例:

从n-1~1枚举i,在i+1~n中找j使得a[i]<=a[j],这样即可确定dp[i]=max(dp[j]+1)(i+1<=j<=n)。

为什么呢,首先此时是逆序,dp[j]已经是确定的值,其次a[i]<=a[j],保证了a[i]能接在以a[j]为开头的不下降子序列中。

有了状态转移方程,代码就很好写了,还有一些小坑,都写在代码注释里了:

#include<bits/stdc++.h>
using namespace std;
const int M=1005;
int a[M],dp[M],c[M]; //注意dp数组所需的空间大小由其意义决定,这里的i的范围是1~n,则dp数组的大小就要略大于n
int main(){
    int n; scanf("%d",&n);
    for(int i=1; i<=n; i++)
        scanf("%d",&a[i]);
    for(int i=1; i<=n; i++) dp[i]=1; //dp数组通常需要根据意义进行初始化,这里每个数据至少的最长不下降子序列长度为1,也就是只有本身
    for(int i=n-1; i>=1; i--){
        int maxn=0;
        for(int j=i+1; j<=n; j++){
            if(a[i]<=a[j]){
                dp[i]=max(dp[i],dp[j]+1); //状态转移方程
            }
        }
    }
    int pos=0;
    for(int i=1; i<=n; i++){
        if(dp[i]>dp[pos]) pos=i; //最后找出以所有值开头的最长不下降子序列中长度最大的,思考一下,可以使用三目运算符使式子更简洁
    }
    printf("max=%d\n",dp[pos]);
    return 0;
}

求长度的问题我们已经解决了,但题目还有一个问题,要求输出最长不下降子序列。这样的问题,很明显可以通过在状态转移过程中加一个数组来统计,这个方法比较方便,可以使用,但状态转移方程的条件要写到if语句中,不能再直接用max函数。

上代码:

#include<bits/stdc++.h>
using namespace std;
const int M=1005;
int a[M],dp[M],c[M]; //c数组用于记录最长不下降子序列(路径)
int main(){
    int n; scanf("%d",&n);
    for(int i=1; i<=n; i++)
        scanf("%d",&a[i]);
    for(int i=1; i<=n; i++) dp[i]=1;
    for(int i=n-1; i>=1; i--){
        int maxn=0;
        for(int j=i+1; j<=n; j++){
            if(a[i]<=a[j]){
                if(dp[j]+1>dp[i]){
                    dp[i]=dp[j]+1; //写到if语句中的状态转移方程
                    c[i]=j; //记录路径
                }
            }
        }
    }
    int pos=0;
    for(int i=1; i<=n; i++){
        if(dp[i]>dp[pos]) pos=i;
    }
    printf("max=%d\n",dp[pos]);
    
    return 0;
}

这里再介绍一种方法,我们可以来观察一下数据:

不难发现,从dp[i]-max的位置开始向后扫,依次找到的最靠前的dp[i]递减序列(图中即dp[i]=8,7,6,5……递减),即为最长不下降子序列。这一方法的正确性不难证明,在后续做“拦截导弹(noip1999)”时也会用到这一原理。

再来看一下此方法的代码(部分):

int x=pos;
    printf("%d ",a[pos]);
    for(int i=pos+1; i<=n ;i++)
        if(dp[i]==dp[pos]-1){
            pos=i;
            printf("%d ",a[pos]);
        }

好啦,这就是本题的讲解。下次再见!

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值