P1020 [NOIP 1999 提高组] 导弹拦截

P1020 [NOIP 1999 提高组] 导弹拦截 - 洛谷

P1020 [NOIP 1999 提高组] 导弹拦截 - 洛谷

题解来自洛谷题解,仅仅用于自己整理知识

这段代码是 NOIP1999 提高组第一题「导弹拦截系统」的一个 高效 O(n log n) 解法,目标是解决两件事:

  • 最多能拦截多少导弹(只有一套系统)

    → 即求一个 最长不升子序列(Longest Decreasing Subsequence,LDS) 的长度

  • 拦截所有导弹最少需要几套系统

    → 即将整个序列划分成最少数量的 不升子序列

✅ 解题思路概览

这段代码用了经典的 贪心 + 二分 + tail 数组维护方式(也是 LIS 的优化思路,反过来处理即可用于 LDS)。

复制代码
const int N = 1e7 + 10;
int n, res, a[N], f[N];
  • a[N]:存储导弹高度

  • f[N]:辅助数组,类似于"每种长度的序列的末尾最小值"

  • n:导弹总数

  • res:结果值(最长长度)

读入输入+数据反转

复制代码
while(cin >> x) a[++n] = x;
reverse(a + 1, a + n + 1);

为什么要反转 a[1...n]

因为原题求的是 最长不升子序列 (LDS)

而 LIS(最长上升子序列)的经典做法就是用 lower_bound 来查找位置。

把问题反转之后求 LIS,就等价于在原序列上求 LDS。

第一问:最长不升子序列(LDS)长度

复制代码
memset(f, 0x3f, sizeof(f)); // 初始化 f[] 为无穷大
for(int i = 1; i <= n; i++) {
    int l = 1, r = i;
    while(l < r) {
        int mid = (r + l) / 2;
        if(f[mid] > a[i]) r = mid;
        else l = mid + 1;
    }
    f[l] = min(f[l], a[i]);
    res = max(res, l);
}
cout << res << endl;

✅ 说明:

我们想维护一个数组 f[i]

  • 表示长度为 i 的不升子序列末尾的最小值

🚀 每次二分查找:

找的是 f[pos] > a[i] 的最小 pos,因为反转后问题就成了找 最长上升子序列

  • 如果找到,就替换 f[pos] 为更小的值,有利于后续增长

  • 如果找不到,就在末尾新增

🔍 解释(二分查找目标):

寻找最小的下标 l,使得 f[l] > a[i]

换句话说:

  • 找到一个位置 l,可以插入 a[i],以维持 f[1...l] 是严格递减的

  • 这是反转过的最长上升子序列的技巧 ------ 用 lower_bound 思路求 LDS

✅ 为什么这样做?

我们维护 f[] 的单调性,使得:

  • f[i] 越小越好 → 更容易接后面的更小值

  • 如果 a[i] 能接在某个 f[l] 之后,就更新它(使结尾更优)

f[l] = min(f[l], a[i]);

🔍 解释:

a[i] 放入 f[l] 这个位置(更新为较小值):

  • 表示存在一个长度为 l 的不升子序列,其结尾为 a[i]

  • 如果原来的 f[l] 更大,那现在用更小的 a[i] 替代它,有利于后续元素加入

第二问:最少系统数(最少不升子序列划分)

复制代码
memset(f, 0x3f, sizeof(f));
reverse(a + 1, a + n + 1); // 再反回来
res = 0;
for (int i = 1; i <= n; i++) {
    int l = 1, r = i;
    while (l < r) {
        int mid = (l + r) / 2;
        if (f[mid] >= a[i]) r = mid;
        else l = mid + 1;
    }
    f[l] = min(f[l], a[i]);
    res = max(res, l);
}
cout << res;
问题 方法 目标 二分条件 更新策略
最长不升子序列 LIS on reversed 找最长 f[mid] > a[i] f[l] = min(f[l], a[i])
最少系统数量 贪心组系统 分组 f[mid] >= a[i] f[l] = min(f[l], a[i])