最长上升子序列(提高LIS之有限制的LIS训练)

目录

题目

解析

朴素LIS

lower_bound优化LIS

包含第k个元素的LIS及优化

“二分”求解

从前至后依次求解


题目

题目描述

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

输入

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

输出

如题目所说的序列长度。

样例输入

8 6
65 158 170 299 300 155 207 389

样例输出

4

【数据范围】

0 < N ≤ 200000,0 < K ≤ N

解析

朴素LIS

可以看到这是一道dp的典型题目——LIS。

令dp[i]为以第i个元素结尾的最长的上升子序列的的长度。那么即有转移方程:dp[i] = max \left \{ dp[j] \right \} + 1 (0 < j < i)。也就是说从1到 i - 1 枚举最长的长度来作为第i个的长度,但这里有个条件,那就是a[i] 必须要大于 a[j]。

具体不再赘述,实在有需要的可以自己再看下百度哈。

#include <cstdio>
#define MAXN 100
#define max(a, b) a > b ? a : b

int n, a[MAXN + 5], dp[MAXN + 5], ans;

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

但是可以看到,这是个O (n^{2})的时间复杂度,要是放在这里的话绝对超时,因此我们得需要用一个更快捷的方法。

lower_bound优化LIS

首先我们令dp[i]为长度为i的LIS的末尾元素

首先我们想想可以知道,在多个长度相等的LIS中,肯定是末尾更小的LIS更有潜力,因此在长度相等的情况下,我们肯定会优先选择更小的元素作为末尾对吧。这也就成为了我们dp转移的重要根据。

也就是说,如果a[i] > dp[len],那么就直接dp[++len] = a[i]就可以了,也就是说把a[i]作为更长的LIS的末尾。如果说是小于的话,那么就必须在前面找到第一个比它大的dp,使它更新为a[i]。

有点蒙是不是,当初我也是这样的,但是举例实践了一下后就能明白了:

a[] = 1 2 4 5 4 6 3

先将ans[1]赋值为a[1],代表长度为1的LIS末尾为1。

i = 2,因为a[i]大于了ans[len],所以len++,代表长度为2的LIS末尾为2。

i = 3,因为也大于了,所以同上,不再赘述。

i = 4,同上。

i = 5,因为此时a[i] 小于了ans[len],找到第一个大于等于a[i]的下标,即为3,更新ans[3],代表长度为3的LIS末尾为3。

…………

最后输出len,最长的LIS。

在这里已经把思路讲完了,但是我们应该怎么用代码实现呢?可以看到dp其实是一个有序的上升的数组,所以我们可以用二分来找到第一个大于等于的地方。当然也可以用C++自带的函数:lower_bound。不会的自己去查下百度哈

#include <cstdio>
#include <algorithm>
#define MAXN 100000
using namespace std;
 
int n, a[MAXN + 5], ans[MAXN + 5];
 
int main (){
    scanf ("%d", &n);
    for (int i = 1; i <= n; ++i)
        scanf ("%d", &a[i]);
    int len = 1;
    ans[len] = a[1];
    for (int i = 2; i <= n; ++i){
        if (a[i] > ans[len]){
            ans[++len] = a[i];
        }
        else{
            int pos = lower_bound (ans + 1, ans + 1 + len, a[i]) - ans;
            ans[pos] = a[i];
        }
    }
    printf ("%d\n",len);
}
 

包含第k个元素的LIS及优化

在这里就更复杂些,要把第k个元素也包含进去,那么前面的单纯lower_bound就不够用了。

“二分”求解

因为这个题目求的是包含k的LIS,所以我在看到题后就想到了一种很奇妙 魔性 方法:在k的前面找一次最长下降子序列,然后再在k的后面找一次最长上升子序列,最后再把两个序列的长度相加不就OK了吗?这样的复杂度也才为O (2 * nlogn) = O (nlogn)而已。

当然,在求上升的子序列时是比较好求的,只需要把上面的套进去即可。但是下降的呢?在这里我就被拦住了。本来好不容易想出来一种方法,但是却又 作死 找到了一种数据给pass掉。改来改去最后竟然只能水过样例??!!

在这里其实可以用一个小技巧——greater。(其实这个东西我以前就知道,只不过忘了而已,白白错了好多遍,幸亏旁边的救助)greater表示内置类型从大到小。当然具体我不想讲太多,大家可以到这上面搜一下

演示一下具体用法:

#include <cstdio>
#include <algorithm>
#include <iostream>
using namespace std;

int n, a[25];

int main (){
    scanf ("%d", &n);
    for (int i = 1; i <= n; i++)
        scanf ("%d", &a[i]);
    sort (a + 1, a + 1 + n, greater <int> ());
    for (int i = 1; i <= n; i++)
        printf ("%d ", a[i]);
}

输入:

5
1 5 4 6 7

输出:

7 6 5 4 1

这下能看出来了吧,本来sort是从小到大排序的,但是在这里它却是从大到小排序,可见一斑。所以在找最长下降子序列时就可以借助这个东西了。

#include <cstdio>
#include <algorithm>
#include <iostream>
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 <int> ()) - dp2;
                dp2[pos] = a[i];
            }
        }
    }
    printf ("%d\n", len1 + len2 - 1);
}
 

至于在这里为什么要减一个1呢?是因为dp1和dp2都有一个k,就会多加了一个长度。(应该不会有人问这个吧,就当我在自言自语

从前至后依次求解

这个就比较  简单好想了。只需要 一个 两个预处理就行了,然后就可以规规矩矩的从前往后依次来求LIS,如果上面还有点疑问的童鞋这里就有福音了,可以说这个思路 是个人都能掌握 十分好掌握。当然这里附上一位的链接

#include <cstdio>
#include <algorithm>
#include <iostream>
using namespace std;
#define MAXN 200000
 
int n, k, a[MAXN + 5], dp[MAXN + 5], len;
 
int main (){
    scanf ("%d%d", &n, &k);
    for (int i = 1; i <= n; i++){
        scanf ("%d", &a[i]);
    }
    int i = 1;
    while (i < k && a[i] >= a[k])
        i ++;
    if (i < k)
        dp[++len] = a[i];
    for (; i <= k; i++){
        if (a[i] < a[k]){
            if (a[i] > dp[len])
                dp[++len] = a[i];
            else{
                int pos = lower_bound (dp + 1, dp + 1 + len, a[i]) - dp;
                dp[pos] = a[i];
            }
        }
    }
    i = k + 1;
    while (i <= n && a[i] <= a[k])
        i ++;
    if (i <= n)
        dp[++len] = a[i];
    for (; i <= n; i++){
        if (a[i] > a[k]){
            if (a[i] > dp[len])
                dp[++len] = a[i];
            else{
                int pos = lower_bound (dp + 1, dp + 1 + len, a[i]) - dp;
                dp[pos] = a[i];
            }
        }
    }
    printf ("%d\n", len + 1);
}
 

 

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值