最长上升子序列

今天,给大家一份惊喜

目录

目录

目录

题目描述

输入

输出

样例输入

样例输出

先谈谈最长上升子序列

给一份儿代码

优化

操作

lower_bound

如何在一个从大到小的数组中使用呢?

秀一份代码

思路篇

代码篇

代码 1(手动模拟 lower_bound)

代码 2 (用 STL ---- lower_bound)

代码 3

闲谈篇

题目

描述

输入

输出

样例输入

样例输出

代码 1.0

解析

代码 2


题目描述

给出一个长度为N的整数序列,求出包含它的第K个元素的最长上升子序列。

输入

第一行两个整数N, K
第二行N个整数

输出

如题目所说的序列长度。

样例输入

Copy (如果复制到控制台无换行,可以先粘贴到文本编辑器,再复制)

8 6
65 158 170 299 300 155 207 389

样例输出

4

先谈谈最长上升子序列

显然这是一道 dp 题,我们用一个 f 数组

f[i] : 以第 i个数字结尾的最长上升子序列 

如果是这样,我们用双重循环,暴力求解即可

i -- 1 \rightarrow n

j -- 1 \rightarrow i - 1

这样 j 就相当于一层暴搜枚举答案(O(n^2)

给一份儿代码

#include
#include
#define M 2000000 + 5
#define reg register

int a[M],f[M],b[M];
int ans,n,k;

int main(){
    scanf("%d",&n);
    for (reg int i = 1;i <= n; ++ i){
        scanf("%d",&a[i]);
        f[i] = 1;
    }
    for (reg int i = 2;i <= n; ++ i)
        for (reg int j = 1;j < i; ++ j)
            if (a[j] < a[i] && f[j] + 1 > f[i])
                f[i] = f[j] + 1;
    for (reg int i = 1;i <= n; ++ i)
        if (f[i] > ans)
            ans = f[i];
    printf("%d\n",ans);
}

显然以这种方法做,是一定会超时的,我们得想一种更快捷的方法,比如O(n log n)的算法

优化

由百度百科,我们可得:我们改变一下 f 数组的定义

f[i] : 长度为 i 的子序列的结尾的值

而显然,,f[i] 的值越小,对于添加值更有利

因而,f 数组的元素个数即答案。

此时,我们只需 1 \rightarrow n顺序遍历一遍即可把答案求出。

操作

  1. 如果当前要加入的元素 a[i] 大于 f[len] (len : f 数组的元素个数),我们就再 f 数组后面再开一位将 a[i] 存进去(f[ ++ len] = a[i];),因为我们求的是最长上升子序列,越长越好呗
  2. 如果 a[i] 小于 f[len] ,我们就在 f 数组中,找一个恰当的位置,将 a[i] 塞进去,假如位置是 pos,则现在以长度 pos 结尾的上升子序列中,a[i] 这一串的可加入元素性更强。

对于操作(2),你会发现如果这样做的话,时间复杂度差不多也是 O(n^2)

但是调试过一遍的你定然会知道,f 的元素是具有单调性的,即 f[i] < f[i + 1] 。熟练掌握了数据结构的你,定然一下就看出来了,二分即可求解,但是在这里我给大家介绍一个函数 lower_bound
 

lower_bound

lower_bound 是一种通过二分查找的方法在一个从小到大的数组中找到第一个大于等于你与之判断的一个数。

注意 lower_bound 返回的是一个地址,所以调用完后一定要减掉数组名,如:

lower_bound(a + 1,a + 1 + n,a[i]) - a

他所表示的就是在 a 数组的 1 - n 号元素中找到第一个大于等于 a[i] 的元素的位置

如何在一个从大到小的数组中使用呢?

首先你得转变思路

在从小到大的数组中找第一个大于等于它的位置,就等同于在从大到小的数组中找到第一个小于等于它的值

然后,这个方法你只需记住就好(不需要太理解)

lower_bound(a + 1,a + 1 + n,a[i],greater < int > ()) - a

秀一份代码

#include
#define ll long long
#define M 100000 + 5
using namespace std;
ll a[M];
int f[M],n,len;
int main()
{
    scanf("%d",&n);
    for(int i = 1;i <= n;i ++ )
        scanf("%lld",&a[i]);
    f[ ++ len] = a[1];
    for (int i = 2;i <= n;i ++){
        if (a[i] > f[len])
            f[ ++ len] = a[i];
        else
        {
            int s = lower_bound(f,f + len,a[i]) - f;
            f[s] = a[i];
        }
    }
    printf("%d\n",len);
    return 0;
}

思路篇

此题思路其实还是比较简单的,重难点都跟大家说了,此时就只差一个简单的解题思路了(不想多解释)

从  1 \rightarrow k - 1 中,找一个最长的子序列,并保证每个元素都小于 a[k]

从 k + 1\rightarrow n 中,找一个最长的子序列,并保证每个元素都大于 a[k]

最后因为 a[k] 的前面的都小于他,a[k] 后面的都大于他,我们刚好就能将 a[k] 插进这样的一个子序列中,组成一个必定包含 k 的最长上升子序列

代码篇

代码 1(手动模拟 lower_bound)

#include
#include
#define M 2000000 + 5
#define reg register
 
int a[M],f[M];
int ans,len,n,k;
 
int lower_bound (int x){
    int l = 1,r = len;
    while (l < r){
        int mid = (l + r) / 2;
        if (f[mid] < x)
            l = mid + 1;
        else r = mid;
    }
    return r;
}
 
int main(){
    scanf("%d%d",&n,&k);
    for (reg int i = 1;i <= n; ++ i)
        scanf("%d",&a[i]);
    int i = 1;
    while (a[i] >= a[k] && i < k)
        ++ i;
    if (i < k)
        f[ ++ len] = a[i];
    for (i = i + 1;i < k; ++ i){
        if (a[i] < a[k]){
            if (a[i] > f[len])
                f[ ++ len] = a[i];
            else {
                int s = lower_bound(a[i]);
                f[s] = a[i];
            }
        }
    }
    ans = 1 + len;
    memset(f,0,sizeof(f));
    i = k + 1,len = 0;
    while (a[i] <= a[k] && i <= n)
        ++ i;
    if (i <= n)
        f[ ++ len] = a[i];
    for (i = k + 1;i <= n; ++ i){
        if (a[i] > a[k]){
            if (a[i] > f[len])
                f[ ++ len] = a[i];
            else {
                int s = lower_bound(a[i]);
                f[s] = a[i];
            }
        }
    }
    ans += len;
    printf("%d\n",ans);
    return 0;
}

代码 2 (用 STL ---- lower_bound)

#include
#include
#include
#include
#define M 2000000 + 5
#define reg register
using namespace std;
  
int a[M],f[M];
int ans,len,n,k;
  
int main(){
    scanf("%d%d",&n,&k);
    for (reg int i = 1;i <= n; ++ i)
        scanf("%d",&a[i]);
    int i = 1;
    while (a[i] >= a[k] && i < k)
        ++ i;
    if (i < k)
        f[ ++ len] = a[i];
    for (i = i + 1;i < k; ++ i){
        if (a[i] < a[k]){
            if (a[i] > f[len])
                f[ ++ len] = a[i];
            else {
                int s = lower_bound(f + 1,f + 1 + len,a[i]) - f;
                f[s] = a[i];
            }
        }
    }
    ans = 1 + len;
    i = k + 1,len = 0;
    while (a[i] <= a[k] && i <= n)
        ++ i;
    if (i <= n)
        f[ ++ len] = a[i];
    for ( ++ i;i <= n; ++ i){
        if (a[i] > a[k]){
            if (a[i] > f[len])
                f[ ++ len] = a[i];
            else {
                int s = lower_bound(f + 1,f + 1 + len,a[i]) - f;
                f[s] = a[i];
            }
        }
    }
    ans += len;
    printf("%d\n",ans);
    return 0;
}

代码 3

为了帮助同窗,了解到了他的方法(其实差不多)

从 k \rightarrow n 中找一个最长上升子序列

从 k \rightarrow 1 中找一个最长下降子序列

本来都没什么问题,但莫名错了好久

最后经研究发现得:lower_bound 只适用从小到大

于是,便用到了 刚才讲的 greater < int > ()

看代码理解一下吧(从 k 开始的,算了两遍 k ,答案要减一)

#include 
#include 
#include 
using namespace std;
#define MAXN 200000
 
int n, k, a[MAXN + 5], dp1[MAXN + 5], dp2[MAXN + 5], len1, len2;
 
int main (){
    scanf ("%d%d", &n, &k);
    for (int i = 1; i <= n; i++){
        scanf ("%d", &a[i]);
    }
    dp1[++len1] = a[k];
    dp2[++len2] = a[k];
    for (int i = k; i <= n; i++){
        if (a[i] > a[k]){
            if (a[i] > dp1[len1])
                dp1[++len1] = a[i];
            else{
                int pos = lower_bound (dp1 + 1, dp1 + 1 + len1, a[i]) - dp1;
                dp1[pos] = a[i];
            }
        }
    }
    for (int i = k; i; i--){
        if (a[i] < a[k]){
            if (a[i] < dp2[len2])
                dp2[++len2] = a[i];
            else{
                int pos = lower_bound (dp2 + 1, dp2 + 1 + len2, a[i],greater()) - dp2;
                dp2[pos] = a[i];
            }
        }
    }
    printf ("%d\n", len1 + len2 - 1);
}
 

闲谈篇

接下来,我们谈谈与这道题无关的东西

题目

描述

设有由n个整数组成的数列,任意删掉若干后剩下的数列成为子序列。

如果子序列是严格不递减的,则成为不下降子序列。

求该数列的最长不下降子序列的长度。

并输出一组一组符合要求的最长子序列。

输入

两行
第一行n(1≤n≤10000)
第二行n个数,用空格隔开,范围均在(-1000到1000之间)

输出

两行
第一行:最长子序列长度
第二行:符合要求的一组最长子序列,两个数之间用空格隔开,行末不含空格

样例输入

14
3 7 9 16 38 24 27 38 44 49 21 52 63 15

样例输出

11
3 7 9 16 24 27 38 44 49 52 63

这道题,如果用朴素算法的话,也是能过的,不过需要一千多毫秒,只是需要在每次更新值得时候,更新一个前缀,最后通过一个递归即可求出答案,我就不再赘述了,请大家直接看代码吧。

代码 1.0

#include
int n,s[10005],i,j,prev[10005],dp[10005];
void init()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&s[i]);
		dp[i]=1;
		prev[i]=i;
	}
}
int lis()
{
	for(int i=2;i<=n;i++)
		for(j=1;j

解析

接下来我们讲讲 O(n log n)的算法的处理吧。

正如我们所知,f[i] 存的只是长度为 i 的子序列中最小的结尾,但并不是答案。

因而,我们需要另外解法……

第一步:照搬照抄

抄什么呢?朴素算法中的 dp 数组,他存的值正是我们所需的(为后文的暴力求解做铺垫)

第二部:一for求解

我们在结束了求最长上升子序列的 for 循环后,我们可以用一个 for 循环找到刚好比 dp[pos](第一次 pos 为长度为 len 的子序列的第 len 位在 a 数组中的位置 ) 小一的位置,然后更新 pos,并将原 pos 代表位置的数存进 pre 数组对应长度的位置。

而如果你想确保这个数列更靠前,我们可以判断一下,如果 当前位的 dp 值刚好等于 dp[pos] ,就将 pos 更新。

如果想让这个数列的答案尽量小,就判断一下 dp[i] 是否等于 dp[pos],如果是的话再判断 a[pos] 是否大于 a[i] 即可

代码 2

#include
#include
#include
#define reg register
#define M 10000 + 5
using namespace std;
int a[M];
int f[M],n,len,dp[M],pos,print[M];
int main()
{
    scanf("%d",&n);
    for(reg int i = 1;i <= n;i ++ )
        scanf("%d",&a[i]);
    f[ ++ len] = a[1];
    dp[1] = 1;
    for (reg int i = 2;i <= n;i ++){
        if (a[i] >= f[len]){
            f[ ++ len] = a[i];
            dp[i] = len;
            pos = i;
        }
        else
        {
            int s = upper_bound(f + 1,f + len + 1,a[i]) - f;
            f[s] = a[i];
            dp[i] = s;
        }
    }
   if ( ! pos)
    pos = 1;
   print[ ++ print[0]] = pos;
    for (reg int i = pos - 1;i >= 1; -- i){
        if (dp[pos] - 1 == dp[i]){
            pos = i;
            print[ ++ print[0]] = pos;
        }
        if (dp[pos] == dp[i] && a[i] < a[pos]){
            pos = i;
            print[print[0]] = pos;
        }
    }
    printf("%d\n",len);

    for (reg int i = print[0];i > 1; -- i)
        printf("%d ",a[print[i]]);
    printf("%d\n",a[print[1]]);
    return 0;
}

 

你可能感兴趣的:(考试专用题,DP初析)