[线性dp] 拦截导弹(最长上升子序列模型+贪心)

12 篇文章 0 订阅

0. 前言

强相关:

[线性dp] 最长上升子序列(模板题+最长上升子序列模型) 贪心解法

本题进阶版:

[线性dp] 导弹防御系统(最长上升子序列模型+贪心+dfs)

1. LIS +贪心

1010. 拦截导弹

在这里插入图片描述

重点: 线性 dpLIS 问题、贪心

思路:

  • 问题就是最少用多少个下降子序列来覆盖掉整个子序列
  • 首先第一问就是寻找最长的下降子序列。 可以直接转化为 LIS 问题
  • 第二问为贪心问题,首先简单说明下贪心直觉和证明方法
    • 假设现在存在很多个子序列,那么当前遍历到的这个导弹不管接到那个子序列的后面,目前来讲,它都是做这些子序列的结尾元素。那么,能接上去的前提是这些子序列的末尾要大于当前数。根据常理直觉来讲,应该是要接到所有大于当前数的最小末尾子序列的后面,这样高度不会出现断崖式下降。当然,所有的子序列末尾都小于当前高度,就得自己成为一个新的子序列头部
    • 证明该贪心思路:
      • 假设 A 表示贪心法得到的序列个数,B 表示最优解
      • 显然,B<=A
      • 通过反证法证明 A<=B:找到贪心法和最优解所有子序列中第一个数字相同但是位置不同的数,那么由于贪心法一定是所有子序列末尾最小的原则,将该数加到了该子序列的后面,所有贪心法所处的末尾数字一定是小于等于最优解的末尾数字的(在此末尾数字都是添加之前),那么我们可以将这两个子序列后面的所有数字进行一个交换,交换后仍为一个合法方案,最终通过这样的调整,一定能将贪心法调整为最优解形式。并且,这样的调整并没有增加子序列的个数,且最多只需要调整 n 次就可以达到一致,故 A <= B
      • 贪心思路的证明,大多使用调整法或者反证法来进行证明
  • 基于这个思想,第一问就写个 LIS 就行了。第二问实现方式为:
    • 开一个数组 g 来存储当前所有已经开好的子序列结尾的数。枚举当前数 x 时,就每次在 g 数组中寻找大于等于它的最小的数,将其更新就行了。如果这个数比所有 g 中所有数都大,则说明这个导弹高度很高,需要自己成为一个导弹系统,就把它放到 g 数组末尾即可
    • 按照如上操作,g 序列一定是单调上升的。这里涉及到 4 个数,更新位置的那个数是 b,它的前一个是 a,后一个是 c,显然,一开始满足,a<b<=c,三者与 x 的关系是:a<x<=b<=c,那么我们将 b 更新为 x,也是满足 a<x<=c 的,所以序列可以稳定单调上升
    • 其实能够发现,这里的贪心思路及代码实现和 [线性dp] 最长上升子序列(模板题+最长上升子序列模型)LIS 问题的贪心+二分 O ( n l o g n ) O(nlogn) O(nlogn),解法都完全一样。所以这两个问题是等价的,故求最多用多少个上升子序列将序列覆盖和最少用多少个下降子序列将整个序列覆盖的答案是一样的,故这两个问题是对偶问题。所以本题可以将数据范围增加到 10w,必须使用 n l o g n nlogn nlogn 的算法才能 AC

[线性dp] 最长上升子序列(模板题+最长上升子序列模型)LIS 贪心+二分的思路强相关。

代码:

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1005;

int n;
int a[N], g[N];
int f[N];

int main() {
	// stringstream 的方式数据读取
	/*	
	* string line;
    * getline(cin, line);
    * stringstream ssin(line);
    * while (ssin >> h[n]) n ++ ;
	*/
	
    while (cin >> a[n]) ++n;                    // 得到输入数字的个数
    
    int res = 0;
    for (int i = 0; i < n; ++i) {
        f[i] = 1;
        for (int j = 0; j < i; ++j)
            if (a[i] <= a[j])                   // 在此求最长下降子序列
                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 k = 0;
        while (k < cnt && g[k] < a[i]) ++k;     // 找到第一个比自己
        g[k] = a[i];
        if (k >= cnt) cnt ++;
    }
    cout << cnt << endl;
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ypuyu

如果帮助到你,可以请作者喝水~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值