最长上升子序列以及延伸

226 阅读3分钟

最长上升子序列II

题目描述

给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。

  • 输入格式

第一行包含整数 N
第二行包含 N 个整数,表示完整序列。

  • 输出格式

输出一个整数,表示最大长度。

  • 数据范围

1N1000001 \leq N \leq 100000
109数列中的数10910^{-9} \leq 数列中的数 \leq 10^{9}

  • 输入样例:
7
3 1 2 1 8 5 6
  • 输出样例:
4

题解

此数据经过加强所以不能用传统的动态规划的方法去做,时间复杂度为 O(n2)O(n^2) 会超时

这里采用 贪心 + 二分 O(nlogn)O(n\log{n})

  • 贪心思想:用样例来举例,遍历到第二个数 1,我们有两个长度为 1 的最长上升子序列,但是后续的数字如果能接到 3 的后面那么就一定可以接到 1 的后面,所以就没有必要存储数字 3 了 ,所以我们只需存储每一个上升子序列结尾最小的数字,并且维护这样的一个数组 。

并且这个数组是严格单调递增的,证明如下:

证明: 不妨设长度为 5 的上升子序列(简称x)结尾为 a ,长度为 6 的上升子序列(简称y)结尾为 b ,如果 a > b, 那么 那么x5 > y[5], 那么就存在一个长度为 5 的上升子序列,他的结尾比 a 小,所以就跟上面的命题矛盾。

所以我们每次可以去比当前数值小的最大值,并且将这个值放在这个最大值的后面,并且数组单调递增,可以考虑用二分 (PS:并不是一定要单调才能用二分)

  • 具体步骤
    • 维护一个数组 f[N],用来存放每个上升子序列的 最小 结尾值
    • 遍历数组 f ,利用二分查找小于当前值的的最大值 f[i], l = 0, r = len 初试为 0,为这个f 的长度:
      • 若找到: 则覆盖 f[i+1]的位置;
      • 若找不到:扩充数组,其实还是放在 f[i+1] 的位置,但是这里要扩充len;
      • 不管找到找不到,二分查找都会返回一个结果;

代码

#include <iostream>

using namespace std;

const int N = 100010;
int a[N], p[N];

int main() {
    int n;
    cin >> n;
    
    for(int i = 0; i < n; i++) cin >> a[i];
    
    int len = 0;
    
    for(int i = 0; i < n; i++) {
        
        int l = 0, r = len; // 初始值
        
        while(l < r) {
            int mid = (l + r + 1) >> 1;
            if(p[mid] >= a[i]) r = mid - 1;
            else l = mid;
        }
        p[l + 1] = a[i];
        len = max(len, l + 1); // 扩充数组的长度
    }
    cout << len << endl;
    return 0;

}

拦截导弹

题目描述

某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。

但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。

某天,雷达捕捉到敌国的导弹来袭。

由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。

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

  • 输入格式

共一行,输入导弹依次飞来的高度。

  • 输出格式

第一行包含一个整数,表示最多能拦截的导弹数。

第二行包含一个整数,表示要拦截所有导弹最少要配备的系统数。

  • 数据范围

雷达给出的高度数据是不大于 30000 的正整数,导弹数不超过 1000。

  • 输入样例:
389 207 155 300 299 170 158 65
  • 输出样例:
6
2

题解

第一问这里就不赘述了,通过动态规划和上面的贪心+二分都可以解决

这里主要来分析第二问

贪心思想:用样例来举例,389加入系统一,从207开始,我们肯定是希望每个系统的结尾的值越大越好,这样每个系统可以拦截更多的导弹,并且每个系统的值都是递减的,所以我们要查找大于当前值的最小值,并且将其覆盖;如果都小于当前值则只能增加一个新的系统。

上一题是上升子序列,然后存储每一个子序列结尾的最小值,这道题是一个下降子序列,使结尾的值尽可能的打,但都是去 查找并替换大于当前值的最小值,最后输出维护的这个数组。所以两个代码是一样的。

代码

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1010;
int a[N], f[N], g[N];
int n;

int main() {
    
    int res = 0;
    
    while(cin >> a[n]) n++;
    
    for(int i = 0; i < n; i++) {
        f[i] = 1;
        for(int j = 0; j < i; j++) {
            if(a[j] >= a[i])
                f[i] = max(f[i], f[j] + 1);
        }
        res = max(res, f[i]);
    }
    cout << res << endl;
    
    int len = 0;
    for(int i = 0; i < n; i++){
        int l = 0, r = len;
        while(l < r){
            int mid = (l + r + 1) >> 1;
            if(g[mid] >= a[i]) r = mid - 1;
            else l = mid;
        }
        g[l + 1] = a[i];
        len = max(len, l + 1);
    }
    
    
    cout << len << endl;
    
    return 0;
    
}