题目地址:
https://www.acwing.com/problem/content/1012/
某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。某天,雷达捕捉到敌国的导弹来袭。由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。输入导弹依次飞来的高度(雷达给出的高度数据是不大于 30000 30000 30000的正整数,导弹数不超过 1000 1000 1000),计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。
输入格式:
共一行,输入导弹依次飞来的高度。
输出格式:
第一行包含一个整数,表示最多能拦截的导弹数。第二行包含一个整数,表示要拦截所有导弹最少要配备的系统数。
数据范围:
雷达给出的高度数据是不大于
30000
30000
30000的正整数,导弹数不超过
1000
1000
1000
最多能拦截的导弹数其实就是求最长非严格下降子序列。而要拦截所有导弹,其实就是问导弹的高度序列最少能拆分为多少个非严格下降子序列,这个问题实际上是反链分解定理的应用(参考https://blog.csdn.net/qq_46105170/article/details/108616895),定理是:偏序集最少的反链分解的个数等于其最长链的长度。在这题里,其实就是,非严格下降子序列的最少分解个数,等于其最长严格上升子序列的长度。有一个时间复杂度 O ( n log n ) O(n\log n) O(nlogn)的反链分解的算法,大致做法是,用一个数组 f f f维护所有反链的最后一个数(每个反链是非严格下降的序列),然后来一个新数 x x x的时候,找到 f f f中最小的大于等于 x x x的那个数,用 x x x替换之;如果不存在这样的数,就新开一个反链,即 x x x自成一个反链。由数学归纳法可以证明 f f f是单调上升的,所以可以用二分来做。算法正确性证明的话,可以考虑最优解和上述做法的第一个不同的操作,将最优解里 x x x及其后的数形成的序列,和上述解里 x x x及其后的数形成的序列进行交换,交换后仍然是最优解(因为反链个数没变),所以最优解可以经过若干次调整变成上述解,也就证明了上述解就是反链分解的最小划分。求最长非严格下降子序列可以用动态规划做(当然也可以用反链分解来做,答案就是严格上升子序列的最少分解个数)。代码如下:
#include <iostream>
using namespace std;
const int N = 1010;
int a[N];
int f[N];
int n;
int main() {
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], 1 + f[j]);
res = max(res, f[i]);
}
cout << res << endl;
int idx = 1;
f[0] = a[0];
for (int i = 1; i < n; i++) {
// f是单调增的,用二分找到第一个大于等于a[i]的数的下标,找不到就新开一个反链
int l = 0, r = idx - 1;
while (l < r) {
int m = l + (r - l >> 1);
if (f[m] > a[i]) r = m;
else l = m + 1;
}
if (f[l] >= a[i]) f[l] = a[i];
else f[idx++] = a[i];
}
cout << idx << endl;
return 0;
}
时间复杂度 O ( n 2 ) O(n^2) O(n2), n n n是导弹个数,空间 O ( n ) O(n) O(n)。