从导弹拦截到贪心算法:LeetCode 1010题保姆级图解教程(附C++代码)
从导弹拦截到贪心算法:LeetCode 1010题保姆级图解教程(附C++代码)
想象你是一名防空指挥官,面对屏幕上不断跳动的导弹高度数据,必须在瞬间做出决策:是用现有拦截系统迎击,还是紧急启动备用设备?这不仅是军事模拟中的经典场景,更是算法竞赛中贪心策略的绝佳案例。本文将用"导弹拦截"这一生动故事,带你穿透抽象算法概念,掌握LeetCode 1010题的核心解法。
1. 问题本质的双重解读
导弹拦截问题看似复杂,实则包含两个相互关联的子问题:
- 单系统最大拦截量 :相当于寻找最长非上升子序列(Longest Non-Increasing Subsequence)
- 全拦截最小系统数 :本质是求解序列的最小链划分,等价于寻找最长上升子序列长度
这两个问题在算法领域被称为 Dilworth定理 的经典应用。理解这层关系,就能将看似不同的两个问题统一在同一个框架下分析。
关键洞察:拦截系统的特性(后发导弹高度≤前发)恰好定义了非上升序列,而系统间的独立性则对应序列划分的无关性。
2. 最长非上升子序列的DP解法
我们先解决第一个问题——单系统最多能拦截多少导弹。用动态规划可以这样建模:
定义 dp[i] 表示以第 i 枚导弹结尾的最长非上升子序列长度。状态转移方程为:
for (int j = 0; j < i; j++) {
if (h[i] <= h[j]) {
dp[i] = max(dp[i], dp[j] + 1);
}
}
操作步骤详解 :
- 初始化所有
dp[i] = 1(每个导弹自身构成长度为1的序列) - 对于每个导弹
i,检查前面所有导弹j:- 如果
h[i] ≤ h[j],说明i可以接在j后面 - 更新
dp[i]为dp[j]+1的最大值
- 如果
- 最终结果是所有
dp[i]中的最大值
时间复杂度分析 :
- 外层循环
n次,内层循环平均n/2次 - 总复杂度为O(n²),对于n≤1000完全可行
3. 最小系统数的贪心策略
第二个问题要求拦截所有导弹所需的最少系统数,这里展现出 贪心算法 的精妙:
- 维护一个数组
sys记录各系统当前拦截的最低高度 - 对于每枚导弹,寻找能拦截它的最低高度系统:
- 存在可用系统:更新该系统高度为当前导弹高度
- 无可用系统:新增系统,记录当前高度
int cnt = 0; // 系统计数
vector<int> sys; // 各系统当前拦截高度
for (int height : missiles) {
auto it = lower_bound(sys.begin(), sys.end(), height, greater<int>());
if (it != sys.end()) {
*it = height;
} else {
sys.push_back(height);
cnt++;
}
}
贪心选择正确性证明 :
- 每次选择可用的最小高度系统,保留较高系统应对后续可能的高空导弹
- 这种策略确保系统利用率最大化,与建立上升子序列的过程等价
4. 算法优化与边界处理
原始解法有优化空间,特别是第二个问题可以利用STL简化:
优化后的双解法代码框架 :
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
void solve() {
vector<int> missiles;
int height;
while (cin >> height) missiles.push_back(height);
// 问题一:最长非上升子序列
vector<int> dp(missiles.size(), 1);
int max_len = 1;
for (int i = 1; i < missiles.size(); ++i) {
for (int j = 0; j < i; ++j) {
if (missiles[i] <= missiles[j]) {
dp[i] = max(dp[i], dp[j] + 1);
}
}
max_len = max(max_len, dp[i]);
}
// 问题二:最小系统数(贪心)
vector<int> systems;
for (int h : missiles) {
auto it = lower_bound(systems.begin(), systems.end(), h, greater<int>());
if (it != systems.end()) *it = h;
else systems.push_back(h);
}
cout << max_len << endl << systems.size() << endl;
}
常见踩坑点 :
- 输入处理:注意导弹高度可能是单行空格分隔的字符串
- 等值处理:高度相等时仍可拦截(非严格下降)
- 空输入:需考虑边界情况
5. 可视化理解与思维训练
为了加深理解,我们通过具体案例拆解:
导弹序列 :389, 207, 155, 300, 299, 170, 158, 65
问题一求解过程 :
| 导弹 | 389 | 207 | 155 | 300 | 299 | 170 | 158 | 65 |
|---|---|---|---|---|---|---|---|---|
| dp值 | 1 | 2 | 3 | 2 | 3 | 4 | 5 | 6 |
问题二系统变化 :
- 389 → 系统A[389]
- 207 → A[207]
- 155 → A[155]
- 300 → 需要新系统B[300]
- 299 → B[299]
- 170 → A[170]
- 158 → A[158]
- 65 → A[65]
最终系统数:2(A和B)
6. 从特例到通用的思维跃迁
理解这个案例后,可以推广到更一般的场景:
- 调度问题 :类似的任务调度、课程安排等问题都可转化为此类模型
- 序列分析 :股票分析中的波段识别、DNA序列匹配等都有相似结构
- 系统设计 :资源分配、负载均衡等实际问题可借鉴这种贪心策略
在ACM竞赛中,这类问题常有以下变种:
- 要求输出具体拦截方案而非仅数量
- 导弹高度带时间窗口限制
- 系统有启动成本时的最优成本控制
7. 代码实现细节与调试技巧
完整可提交的AC代码需要特别注意:
输入处理优化 :
string line;
getline(cin, line);
istringstream iss(line);
vector<int> missiles;
int height;
while (iss >> height) missiles.push_back(height);
调试建议 :
- 先验证最长子序列计算是否正确
- 打印系统数组观察其变化过程
- 对极端用例测试:
- 严格递减序列(应输出n和1)
- 严格递增序列(应输出1和n)
- 全部等高的序列
性能分析工具 :
g++ -pg solution.cpp -o solution
./solution < input.txt
gprof solution gmon.out > analysis.txt
8. 扩展思考:为什么贪心在此有效
这个问题的精妙之处在于它展现了贪心算法适用的典型场景:
- 最优子结构 :全局最优解包含局部最优解
- 无后效性 :当前决策不影响后续决策环境
- 单调性 :系统数组始终保持有序,便于二分查找
与其他贪心经典问题(如区间调度、霍夫曼编码)对比,可以发现共同特征:
- 都需要某种排序预处理
- 都维护一个关键数据结构(这里是系统高度数组)
- 决策只依赖当前状态而非历史路径
在实际工程中,这种模式可应用于:
- 云资源的自动扩缩容
- 实时系统中的任务调度
- 网络流量中的拥塞控制
更多推荐
所有评论(0)