章节概述
贪心算法在每一步选择中都采取当前状态下最优的决策,从而希望最终结果也是最优的。
贪心与 DP 的核心区别:贪心每步只做一次选择且不回退;DP 考虑所有可能转移,选择全局最优。
贪心能用的情况通常需要满足贪心选择性质和最优子结构。
核心原理
1. 贪心选择性质
问题的最优解可以通过一系列局部最优选择得到。即:在第 k 步做出贪心选择后,剩余子问题的最优解与第 k 步选择组合,仍构成原问题最优解。
2. 贪心正确性证明方法
| 方法 | 思路 | 示例 |
|---|---|---|
| 交换论证法 | 假设存在非贪心最优解,证明交换为贪心解后不会变差 | 排队接水、活动安排 |
| 数学归纳法 | 第 1 步贪心正确,假设前 k 步正确证第 k+1 步 | 哈夫曼编码 |
| 反证法 | 假设贪心不是最优,导出矛盾 | 部分背包 |
3. 贪心常用模式
1. 按某种属性排序(性价比、结束时间、数值大小)
2. 顺序扫描,维护当前状态
3. 每一步贪心决策:
- 排队接水: 按时间升序
- 合并果子: 每次取最小两个 (用堆维护)
- 部分背包: 按性价比降序
- 活动安排: 按结束时间升序
关键数据结构
- C_堆_Heap — priority_queue 实现合并果子、哈夫曼编码 (O(log n) 取最小)
- A_容器_Container — vector 排序后贪心遍历
- Q_排序_八大排序_Sorting — 排序是大多数贪心问题的预处理步骤
P1223 排队接水
题目: n 个人排队打水,每人需要 T[i] 时间。求一种排队顺序使得所有人的平均等待时间最小。
思路: 短作业优先——按打水时间升序排列,等待时间为前面所有人打水时间之和。
#include <iostream>
#include <algorithm>
#include <cstdio>
using namespace std;
struct P { int t, id; };
bool cmp(P a, P b) { return a.t < b.t; }
int main() {
int n;
P p[1005];
cin >> n;
for (int i = 0; i < n; i++) {
cin >> p[i].t;
p[i].id = i + 1;
}
sort(p, p + n, cmp);
double total = 0, cur = 0;
for (int i = 0; i < n; i++) {
cout << p[i].id << " ";
total += cur;
cur += p[i].t;
}
printf("\n%.2f\n", total / n);
return 0;
}P1090 [NOIP2004 提高组] 合并果子
题目: n 堆果子每堆重量 a_i,每次合并两堆消耗体力等于两堆重量之和,求合并成一堆的最小体力消耗。
思路: 每次合并最小的两堆(哈夫曼树思想),用优先队列(小根堆)维护。
#include <iostream>
#include <queue>
using namespace std;
int main() {
int n, ans = 0;
priority_queue<int, vector<int>, greater<int>> pq;
cin >> n;
for (int i = 0, x; i < n; i++) {
cin >> x;
pq.push(x);
}
while (pq.size() > 1) {
int a = pq.top(); pq.pop();
int b = pq.top(); pq.pop();
int sum = a + b;
ans += sum;
pq.push(sum);
}
cout << ans << endl;
return 0;
}P1106 删数问题
题目: 输入一个高精度正整数 n,去掉其中 s 个数字后剩下的数字按原顺序组成新数,求新数的最小值。
思路: 贪心策略: 每次从左到右找第一个比右边大的数字删除(这个数字的”贡献”太大),重复 s 次。
#include <iostream>
#include <string>
using namespace std;
int main() {
string n;
int s;
cin >> n >> s;
while (s--) {
int i = 0;
while (i < (int)n.size() - 1 && n[i] <= n[i + 1]) i++;
n.erase(i, 1);
}
while (n.size() > 1 && n[0] == '0') n.erase(0, 1);
cout << n << endl;
return 0;
}P2240 部分背包问题
题目: 体积为 M 的背包和 N 件物品,每件物品有体积 v_i 和价值 w_i,物品可任意切割(取体积的一部分则按比例获得价值)。求背包能装的最大总价值。
思路: 按性价比(价值/体积)降序排列,贪心装入,最后一件可能切割。
#include <iostream>
#include <algorithm>
#include <cstdio>
using namespace std;
struct Item { double w, v, ratio; };
bool cmp(Item a, Item b) { return a.ratio > b.ratio; }
int main() {
int n; double m;
Item a[105];
cin >> n >> m;
for (int i = 0; i < n; i++) {
cin >> a[i].w >> a[i].v;
a[i].ratio = a[i].v / a[i].w;
}
sort(a, a + n, cmp);
double ans = 0;
for (int i = 0; i < n && m > 1e-9; i++) {
double take = min(m, a[i].w);
ans += take * a[i].ratio;
m -= take;
}
printf("%.2f\n", ans);
return 0;
}推荐练习题(洛谷)
相关技巧
- 排序: 贪心通常需要排序预处理
- 二分答案: 贪心算法常用于二分答案的 check 函数
- 搜索: 贪心是搜索的剪枝/近似的替代方案
- 前缀和: 区间贪心问题配合前缀和优化
- C_堆_Heap: priority_queue 实现合并果子、哈夫曼编码
- A_容器_Container: vector 排序后贪心遍历
多平台练习
| 洛谷 | 本题单 | 竞赛基础 |
| POJ (北大) | PKU JudgeOnline | 经典题目,适合巩固 |
| HDU (杭电) | HDU OJ | 暑期多校训练 |
| Codeforces | Codeforces | 国际竞赛,适合提升 |