拦截导弹

拦截导弹

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

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

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

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

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

输入格式

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

输出格式

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

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

数据范围

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

输入样例:

389 207 155 300 299 170 158 65

输出样例:

6
2

 

解题思路

  这题有两问。第一问就是求最长的一个单调下降的子序列。第二问是求最少用多少个单调下降的子序列把原序列覆盖住。

  第一问就是序列类型的dp,主要是第二问不好想,第二问的做法是贪心。

  对于原序列的第一个数,此时还没有任何子序列,因此需要单独开一个子序列来存放第一个数。对于原序列的第二个数有两种选择,一个是把这个数接在已有的子序列后面,另一个是单独开一个子序列。不管我们选择哪个,这个数一定是现有的某个子序列的最后一个数。我们希望子序列的最后一个数越大越好,因为最后一个数越大,它后面能接的数也越大,因此我们希望在将第二个数接入某个子序列后,剩下的子序列的数的尽可能的大。因此我们可以从结尾大于等于第二个数的子序列中,选择结尾最小的那个子序列,然后把第二个数接到这个子序列的后面。如果不存在结尾大于等于第二个数的子序列,那么就为第二个数开一个新的子序列。往后的数的做法也以此类推。

  因此贪心的流程是:从前往后扫描每一个数,对于每一个数如果:

  1. 现有的子序列的结尾都小于当前数,则创建新的子序列。
  2. 将当前数放到结尾大于等于它的最小的子序列后面。

  下面证明这种贪心的做法是对的。

  因为贪心解得到的结果是合法的,因此有贪心解得到的子序列数量大于等于最优解得到的子序列数量。

  假设最优解对应的方案和贪心解得到的方案不同,找到第一个不同的数,这个数在贪心解和最优解的位置不同。

  因此只要最优解和贪心解得到方案不一样,找到第一个不一样的位置,进行交换使得当前数与贪心解的一样。因此最多调整$n$次就会使得最优解变成贪心解,而在每一次的交换的过程中不会增加子序列的个数(可能会减少)。因此最优解得到的子序列数量大于等于贪心解得到的子序列数量。

  因此可以说明贪心解得到的子序列数量等于最优解得到的子序列数量。

  实现的方法是,开一个数组$g \left[ ~ \right]$用来存放每一个子序列的结尾的那个数,$g$数组一定是单调递增的。从头开始遍历$g \left[ ~ \right]$找到第一个大于等于当前数的子序列。

  AC代码如下:

 1 #include <bits/stdc++.h>
 2 using namespace std;
 3 
 4 const int N = 1010;
 5 
 6 int a[N], f[N];
 7 int g[N], sz;
 8 
 9 int main() {
10     int n = 0;
11     while (~scanf("%d", a + n)) {
12         n++;
13     }
14     
15     int ret = 0;
16     for (int i = 0; i < n; i++) {
17         f[i] = 1;
18         for (int j = 0; j < i; j++) {
19             if (a[j] >= a[i]) f[i] = max(f[i], f[j] + 1);
20         }
21         ret = max(ret, f[i]);
22     }
23     
24     for (int i = 0; i < n; i++) {
25         int j = 0;
26         while (j < sz && g[j] < a[i]) { // 找到第一个结尾大于等于a[i]的子序列
27             j++;
28         }
29         if (j == sz) sz++;  // 说明不存在结尾小于a[i]的子序列,需要新开一个
30         g[j] = a[i];
31     }
32     
33     printf("%d\n%d", ret, sz);
34     
35     return 0;
36 }

  观察第二个问题的代码,可以发现与最长上升子序列问题的贪心解法很像,事实上代码是一样的,意味着对于一个序列,求最少用多少个单调下降的子序列把原序列覆盖住这个问题,与找到序列的一个最长的严格单调上升的子序列这个问题是等价的。Dilworth定理就是描述这个问题,Dilworth定理:对偏序集$<A,\leq>$,设$A$中最长链的长度是$n$,则将$A$中元素分成不相交的反链,反链个数至少是$n$。因此完全可以把第二问的代码换成求最长上升子序列的代码。

  AC代码如下:

 1 #include <bits/stdc++.h>
 2 using namespace std;
 3 
 4 const int N = 1010;
 5 
 6 int a[N], f[N];
 7 
 8 int main() {
 9     int n = 0;
10     while (~scanf("%d", a + n)) {
11         n++;
12     }
13     
14     int ret = 0;
15     for (int i = 0; i < n; i++) {
16         f[i] = 1;
17         for (int j = 0; j < i; j++) {
18             if (a[j] >= a[i]) f[i] = max(f[i], f[j] + 1);
19         }
20         ret = max(ret, f[i]);
21     }
22     printf("%d\n", ret);
23     
24     ret = 0;
25     for (int i = 0; i < n; i++) {
26         f[i] = 1;
27         for (int j = 0; j < i; j++) {
28             if (a[j] < a[i]) f[i] = max(f[i], f[j] + 1);
29         }
30         ret = max(ret, f[i]);
31     }
32     printf("%d", ret);
33     
34     return 0;
35 }

 

参考资料

  AcWing 1010. 拦截导弹(算法提高课):https://www.acwing.com/video/363/

  狄尔沃斯定理:https://baike.baidu.com/item/%E7%8B%84%E5%B0%94%E6%B2%83%E6%96%AF%E5%AE%9A%E7%90%86/18900593

posted @ 2022-07-27 22:40  onlyblues  阅读(366)  评论(0编辑  收藏  举报
Web Analytics