贪心
💡 核心思想
贪心算法在每一步选择中都做出当前看起来最优的选择,期望局部最优能导致全局最优。贪心不是对所有问题都有效,但一旦可以贪心,代码通常非常简洁。关键是要能证明"局部最优 → 全局最优"。
贪心最核心的问题不是"怎么贪",而是"能不能贪"。证明贪心正确性的常用方法有三种:交换论证法(任意最优解都可以通过交换变成贪心解)、归纳法、反证法。竞赛中时间紧,如果不能严格证明,至少要用反例测试——想一个贪心策略,然后试着构造一个让贪心失败的反例。如果想不到反例,大概率贪心是对的。
🎯 直觉理解
想象你是收银员,要给顾客找零,用最少的硬币。贪心策略是"每次给最大面额的硬币"。对于人民币面额(1,5,10,50,100)这个贪心是对的。但如果面额是 {1,3,4},要找6块——贪心会 4+1+1=3枚,但最优是 3+3=2枚。所以贪心不一定对,需要验证。
📝 算法流程
- 确定贪心策略:每步选择什么?
- 证明正确性:交换论证/归纳/反证
- 实现:通常涉及排序+逐个处理
$$\text{贪心正确性需要证明:局部最优选择不会影响全局最优解的存在}$$
📊 复杂度分析
| 指标 | 复杂度 |
|---|---|
| 时间 | 通常 $O(n \log n)$(排序为主) |
| 空间 | $O(n)$ |
💻 参考实现(C++)
C++ (C++17)
#include <bits/stdc++.h>
using namespace std;
// 经典贪心:区间调度(最多选多少不重叠区间)
int main() {
int n; cin >> n;
vector<pair<int,int>> seg(n);
for (auto& [l, r] : seg) cin >> l >> r;
// 按右端点排序,每次选结束最早的
sort(seg.begin(), seg.end(), [](auto& a, auto& b) { return a.second < b.second; });
int cnt = 0, end = INT_MIN;
for (auto& [l, r] : seg) {
if (l >= end) { cnt++; end = r; }
}
cout << cnt << endl;
return 0;
}⚠️ 常见坑点
没有证明贪心正确性就使用,导致答案错误
排序关键字选错(如区间调度应按右端点而非左端点)
贪心策略描述不够精确
混淆贪心与动态规划——能贪心的不用 DP,不能贪心的硬贪会WA
📚 相关题目
| 题目 | 来源 | 难度 | 备注 |
|---|---|---|---|
| P1223 排队接水 | 洛谷 | CSP-J | 排序+贪心 |
| P1090 合并果子 | 洛谷 | CSP-J | 哈夫曼/优先队列贪心 |