AcWing 1010. 拦截导弹----最长上升子序列的衍生应用 --图示分析

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

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

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

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

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

输入格式

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

输出格式

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

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

数据范围

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

输入样例:

389 207 155 300 299 170 158 65

输出样例:

6
2

题解

本题关键点是一套拦截系统每一发拦截炮弹都低于上一次拦截炮弹的高度 拦截的高度是逐次下降的
但所要拦截的导弹的高度并不一致 有高有低
第一问让你求一套拦截系统最多能拦截的导弹数量
其实就是让你求在给出的导弹的序列里 最长下降序列的最大值 就是我们最多能拦截的导弹数量
最长下降序列 反过来就是最长上升序列
我们只需要由模板向回搜索比自己小的序列 改为向回搜索之前发射的导弹里高度更高的导弹的子序列 将这枚导弹加入该序列
直接套模板
前导题 https://www.acwing.com/activity/content/code/content/8161035/
我们先解决第一问

//问题1 最大拦截导弹数量
    for (int i = 0; i < n; i ++ )
    {
        f[i] = 1; //初始化每个子序列长度为1 至少可以拦截他自己
        for (int j = i - 1; j >= 0; j -- ) //当 当前序列结尾是i时往回搜索序列 之前的序列都是在前面循环内处理好的
        {
            if (q[j] >= q[i]) //往回搜索 找比当前i导弹 高度更高的导弹序列 如果某一子序列结尾导弹j高度大于i 在其结尾加上i导弹 并将该序列结尾更新为i 也就是f[j] + 1
                f[i] = max(f[i], f[j] + 1);
        }

        res = max(res, f[i]);  //存一下当前最长的子序列 也就是最多可以拦截的导弹数量
    }

    cout << res << endl;

接下来解决第二问
需要多少套系统才能拦截所有导弹
也就是要求我们用最少的最长下降子序列对原导弹数组进行全覆盖
先定义以j结尾的导弹子序列是下降排列 j导弹是当前序列高度最低的导弹
当我们当前扫描到序列内的某一枚导弹
该导弹有两种选择
1. 如果现有的导弹序列 结尾的导弹高度都低于该导弹 那我们只能重新开一个子序列 并且以该导弹结尾 (j序列内的导弹不能并进来 因为他们比i先发射了)
2. 存在多个比i高度相等或更高的导弹序列结尾j 那我们就把i放进高度最低的序列里面 也就是放进 结尾j导弹与i导弹高度差最小的序列里 这样我们才能更方便放后面的导弹

注意 我们并不是真存了这么多子序列 我们只用g[j]记录j号子序列的结尾导弹高度
如果i放不进来 就新建一个子序列 放到g[j++]里面 作为新的j+1号子序列结尾
如果能放进来 就把子序列j号子序列结尾更新为i导弹的高度
母题 https://www.acwing.com/problem/content/description/898/

#include <bits/stdc++.h>

using namespace std;

const int N = 1010;

int n;
int q[N], g[N];
int f[N];
int res;

int main()
{
    while (cin >> q[n]) n ++ ; //本题输入数据的特殊点 他没有告诉你一共有多少导弹 所以我们采用这种特殊的方法输入
                               //还有一种stringstream的方法

    //问题1 最大拦截导弹数量
    for (int i = 0; i < n; i ++ )
    {
        f[i] = 1; //初始化每个子序列长度为1 至少可以拦截他自己
        for (int j = i - 1; j >= 0; j -- ) //当 当前序列结尾是i时往回搜索序列 之前的序列都是在前面循环内处理好的
        {
            if (q[j] >= q[i]) //往回搜索 找比当前i导弹 高度更高的导弹序列 如果某一子序列结尾导弹j高度大于i 在其结尾加上i导弹 并将该序列结尾更新为i 也就是f[j] + 1
                f[i] = max(f[i], f[j] + 1);
        }

        res = max(res, f[i]);  //存一下当前最长的子序列 也就是最多可以拦截的导弹数量
    }

    cout << res << endl;

    //问题2 需要多少套系统才能拦截所有导弹
    //也就是要求我们用最少的最长下降子序列对原导弹数组进行全覆盖
    //g[i]存的是第i号子序列结尾的高度最低的导弹的高度
    int cnt = 0; //当前创建的子序列个数
    for (int i = 0; i < n; i ++ )
    {
        int k = 0; //当前枚举到的以k导弹结尾的子序列 每次循环前清空 保证从0号子序列的结尾开始搜
        while (k < cnt && g[k] < q[i]) k ++ ;  //如果我们还没有枚举完所有序列个数 并且 当前枚举到的序列结尾k 高度小于 当前导弹i 证明不能将i放进该序列 K++ 继续枚举下一个序列 看能不能放进i
        if (k >= cnt) g[cnt ++ ] = q[i]; //如果都把当前序列数量搜完了还没有任何一个序列能放下i 我们就新开一个序列来存i
        else g[k] = q[i]; //当我们没有枚举完所有序列就找到了能放i的序列k 就将该序列的结尾就更新为i导弹的高度
    }

    cout << cnt << endl;

}

用图来分析一下这个序列怎么接的
fad4c72a188be14f88f89879b45069d.jpg


法二 更低的时间复杂度 使用二分 \(O(nlogn)\)

#include <bits/stdc++.h>

using namespace std;

const int N = 5e4 + 10;

int n;
int q[N], g[N];
int f[N];
int res;

int main()
{
    while (cin >> q[n]) n ++ ;

    for (int i = 0; i < n; i ++ )
    {
        f[i] = 1; 
        for (int j = i - 1; j >= 0; j -- ) 
        {
            if (q[j] >= q[i]) 
                f[i] = max(f[i], f[j] + 1);
        }

        res = max(res, f[i]);  
    }

    cout << res << endl;


    int cnt = 0; //当前创建的拦截系统个数
    for (int i = 0; i < n; i ++ )
    {
        int l = 0, r = cnt;
        //数组内存的是第i个拦截系统能处理的最低高度
        //从数组中找到一个 最小的大于等于 当前i导弹的高度 的系统处理高度 也就是二分左边界
        while (l < r)
        {
            int mid = l + r >> 1;
            if (q[i] <= g[mid]) r = mid;
            else l = mid + 1;
        }
        if (g[r] < q[i]) r ++ ; //处理边界 当i的高度大于数组内任意一个系统能处理的高度时 再开一个系统
        g[r] = q[i]; //将该导弹放入找到的系统 或者 新建的系统 作为当前系统所能处理的最低高度
        cnt = max(cnt, r); //存系统数量
    }

    cout << cnt << endl;

}
posted @ 2024-04-16 16:30  MsEEi  阅读(30)  评论(0)    收藏  举报