从导弹拦截到贪心算法:AcWing 1010题保姆级讲解(附C++代码)
从导弹拦截到贪心算法:AcWing 1010题保姆级讲解(附C++代码)
导弹拦截问题看似是一个军事领域的应用场景,实则是算法学习中一个经典的贪心算法和动态规划结合的案例。很多初学者在面对这个问题时,往往会被其实际背景所迷惑,而忽略了背后隐藏的算法本质。今天我们就来彻底拆解这个问题,看看如何从导弹拦截联想到会议安排,再到最长上升子序列,最终用简洁的代码解决这个看似复杂的问题。
1. 问题本质与算法映射
导弹拦截问题的核心可以抽象为两个部分:
- 单系统最大拦截数量 :即求一个序列的最长非上升子序列(Longest Non-Increasing Subsequence)
- 全拦截最少系统数量 :即求一个序列的最少划分,使得每个划分都是非上升序列
这实际上与计算机科学中的多个经典问题有着深刻的联系。比如:
- 会议安排问题 :如何用最少的会议室安排所有会议
- 字符串拼接问题 :如何用最少的字符串拼接出目标字符串
- 飞机调度问题 :如何用最少的登机口安排所有航班
这些问题的共同特点是: 都需要在满足一定约束条件下,寻找最优的资源分配方案 。
2. 最长非上升子序列的DP解法
对于第一个问题——求单系统最大拦截数量,我们可以使用经典的动态规划方法来解决。定义一个数组 f[] ,其中 f[i] 表示以第 i 个导弹结尾的最长非上升子序列的长度。
状态转移方程为:
f[i] = max(f[i], f[j] + 1) // 对于所有j < i且h[j] >= h[i]
这个方程的含义是:对于每个导弹 i ,我们检查前面所有比它高的导弹 j ,看看能否将 i 接在 j 后面形成更长的序列。
实际代码实现如下:
int res = 0;
for(int i = 0; i < n; i++) {
f[i] = 1;
for(int j = 0; j < i; j++) {
if(h[i] <= h[j]) {
f[i] = max(f[i], f[j] + 1);
}
}
res = max(res, f[i]);
}
注意:这里的时间复杂度是O(n²),对于n=1000来说完全足够。但如果数据规模更大,可以考虑使用二分查找优化到O(nlogn)。
3. 最少系统数的贪心解法
第二个问题——求拦截所有导弹需要的最少系统数量,才是本题的精髓所在。这个问题可以转化为: 如何用最少的非上升序列覆盖整个序列 。
贪心策略如下:
- 维护一个数组
q[],记录当前每个系统能拦截的最低高度(即最后一个拦截的导弹高度) - 对于每个新导弹,找到第一个能拦截它的系统(即
q[k] >= h[i]的最小k) - 如果找不到这样的系统,就新增一个系统
- 更新对应系统的拦截高度
这个策略的正确性基于以下观察:
- 系统拦截高度数组
q[]始终保持升序 - 每次都将新导弹放入能满足条件的最左边的系统,为后续导弹留出更多空间
代码实现:
int cnt = 0;
for(int i = 0; i < n; i++) {
int k = 0;
while(k < cnt && h[i] > q[k]) k++;
if(k == cnt) {
q[cnt++] = h[i];
} else {
q[k] = h[i];
}
}
有趣的是,这个问题的答案恰好等于原序列的**最长上升子序列(LIS)**的长度。这是一个非常美妙的数学性质,也是很多算法竞赛中常考的考点。
4. 完整代码实现与解析
将上述两部分结合起来,我们得到完整的解决方案:
#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int h[N], f[N], q[N];
int n;
int main() {
string line;
getline(cin, line);
stringstream ss(line);
while(ss >> h[n]) n++;
// 第一部分:求最长非上升子序列
int max_len = 0;
for(int i = 0; i < n; i++) {
f[i] = 1;
for(int j = 0; j < i; j++) {
if(h[i] <= h[j]) {
f[i] = max(f[i], f[j] + 1);
}
}
max_len = max(max_len, f[i]);
}
// 第二部分:求最少系统数
int sys_cnt = 0;
for(int i = 0; i < n; i++) {
int pos = 0;
while(pos < sys_cnt && h[i] > q[pos]) pos++;
if(pos == sys_cnt) {
q[sys_cnt++] = h[i];
} else {
q[pos] = h[i];
}
}
cout << max_len << endl << sys_cnt << endl;
return 0;
}
代码中的几个关键点:
- 输入处理 :使用
stringstream处理可能含有不定数量整数的输入行 - 双问题求解 :分别计算最大拦截数量和最少系统数量
- 数组复用 :
q[]数组既用于记录系统状态,又不需要额外初始化
5. 算法优化与扩展思考
虽然上述解法已经能够AC本题,但我们还可以进一步思考优化空间:
-
二分查找优化 :对于最长上升子序列问题,可以使用二分查找将时间复杂度从O(n²)降到O(nlogn)
优化后的查找部分代码:
int pos = lower_bound(q, q + sys_cnt, h[i]) - q; -
统一解法 :注意到两个问题其实都是关于子序列的,可以尝试寻找更统一的解法
-
实际问题扩展 :考虑导弹拦截系统的其他变种,比如:
- 系统有启动延迟
- 导弹有不同的威胁值
- 拦截有成功率限制
这些扩展问题在ACM/ICPC等竞赛中经常出现,理解基础问题的本质有助于解决更复杂的变种。
更多推荐
所有评论(0)