贪心
什么是贪心
在每次决策的时候都采取当前意义下的最优策略,一般贪心问题的难点在于最优策略的选择。
例题:有 \(n\) 项工作,每项工作从 \(s_i\) 时间开始,\(t_i\) 时间结束,对于每项工作你都可以选择是否要参加,若参加则必须全程参与。那么在不能同时参与多个工作的情况下,最多可以参加几个工作?(\(n \le 10^5; s_i,t_i \le 10^9\))
懒狗策略(错误)
时间越长的工作越耽误时间,那么考虑按照工作时长排序,先选择做工作时间短的。
反例:(1, 4), (3, 5), (4, 7) 三个工作,按照这个策略会优先选择 (3, 5) 这个工作,从而剩下的工作都做不了了;而实际上最好的方案是做 (1, 4) 和 (4, 7) 这两个工作。
卷王策略(错误)
开始工作得越早,就能做更多的工作,考虑按照 \(s_i\) 排序,先做开始时间造的工作。
反例:(1, 10), (3, 5), (7, 9) 三个工作,按照这个策略会优先选择 (1, 10) 这个工作,从而剩下的工作都做不了了;而实际上最好的方案是做 (3, 5) 和 (7, 9) 这两个工作。
正确策略
按照 \(t_i\) 排序,先做结束时间早的。
证明:优先考虑结束时间可以在选择工作数量相同的情况下,为后续的选择提供更多的时间。
例题:CF1768C Elemental Decompress
题意:给定一个序列 \(a\),请构造两个排列 \(p, q\),使得 \(a_i = \max (p_i, q_i)\),可能无解。
数据范围:\(n \le 200000\)
解题思路
可以按照 \(a[i]\) 的大小,按从大到小的顺序依次填数。
比如 \(a = [5, 3, 4, 2, 5]\)
首先填第一个数和第五个数(\(5\) 最大),因为要使得 \(a\) 中两个位置为 \(5\),所以不妨让 \(p[1] = 5, q[5] = 5\)
接着填第三个数,只需要一个 \(4\),不妨让 \(p[3] = q[3] = 4\)
同理,让 \(p[2] = q[2] = 3, p[4] = q[4] = 2\),此时 \(p\) 和 \(q\) 的一部分位置已经填过数字,还剩一些数字没有使用;
重复从大到小的填数顺序,将 \(p\) 和 \(q\) 中剩余的数也同样从大到小把一开始没填的位置填上,因此 \(p[5] = 1, q[1] = 1\)
注意按以上策略填完数字后还需要重新检查一遍是否满足要求,不满足说明无解。
参考代码
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
const int N = 200005;
int a[N], p[N], q[N];
bool u1[N], u2[N];
vector<int> idx[N];
int main()
{
int t;
scanf("%d", &t);
while (t--) {
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
idx[i].clear(); p[i] = q[i] = 0;
u1[i] = u2[i] = false;
}
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
idx[a[i]].push_back(i);
}
bool flag = true;
for (int i = n; i >= 1; i--) {
if (idx[i].size() > 2) {
flag = false; break;
}
if (idx[i].size() == 2) {
p[idx[i][0]] = i; q[idx[i][1]] = i;
u1[i] = u2[i] = true;
} else if (idx[i].size() == 1) {
p[idx[i][0]] = q[idx[i][0]] = i;
u1[i] = u2[i] = true;
}
}
if (!flag) {
printf("NO\n"); continue;
}
int num1 = n, num2 = n;
for (int i = n; i >= 1; i--) {
if (idx[i].size() == 2) {
int i1 = idx[i][0], i2 = idx[i][1];
while (num1 >= 0 && u1[num1]) num1--;
p[i2] = num1; u1[num1] = true;
while (num2 >= 0 && u2[num2]) num2--;
q[i1] = num2; u2[num2] = true;
}
}
for (int i = 1; i <= n; i++)
if (a[i] != max(p[i], q[i])) {
flag = false; break;
}
if (flag) {
printf("YES\n");
for (int i = 1; i <= n; i++) printf("%d%c", p[i], i == n ? '\n' : ' ');
for (int i = 1; i <= n; i++) printf("%d%c", q[i], i == n ? '\n' : ' ');
} else printf("NO\n");
}
return 0;
}
例:P1090 [NOIP2004 提高组] 合并果子
解题思路
不难发现,每次合并最小的两堆果子就是最优的选择。所以我们可以把所有的果子数目都放到一个小根堆里,每次掏出最小的两堆,计入答案后再把这两堆合并后的数量放回堆中。
哈夫曼编码

选择题:以下对数据结构的表述不恰当的是?
- A. 队列是一种先进先出(FIFO)的线性结构
- B. 哈夫曼树的构造过程主要是为了实现图的深度优先搜索
- C. 散列表是一种通过散列函数将关键字映射到存储位置的数据结构
- D. 二叉树是一种每个节点最多有两个子节点的树结构
答案
不恰当的表述是 B。哈夫曼树是一种特殊的二叉树,它的主要用途是数据压缩。通过贪心算法构建哈夫曼树,可以为出现频率高的字符分配较短的编码,为频率低的字符分配较长的编码,从而实现最优的前缀编码,达到数据压缩的目的,它与图的深度优先搜索算法没有直接关系。
选择题:下列哪个问题不能用贪心法精确求解?
- A. 霍夫曼编码问题
- B. 0-1 背包问题
- C. 最小生成树问题
- D. 单源最短路径问题
答案
B。
贪心法是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是全局最好或最优的算法策略。它不从整体最优上加以考虑,所做的只是在某种意义上的局部最优选择。
霍夫曼编码是一种用于无损数据压缩的熵编码算法。其核心思想是:将出现频率高的字符用较短的编码,将出现频率低的字符用较长的编码,从而使得最终编码的总长度最短。霍夫曼算法是一个典型的贪心算法,它每次都选取当前集合中频率最小的两个节点(字符或子树)合并成一个新的父节点,新节点的频率是两个子节点频率之和。这个过程不断重复,直到所有节点都合并成一棵树。这个贪心策略被证明是能够得到最优解(即总长度最短的编码)的。
最小生成树问题是在一个加权无向图中,找到一棵连接所有顶点的、总权重最小的树。解决这个问题的两个经典算法都是贪心算法:Prim 算法每次都选择连接已选顶点集合和未选顶点集合的权重最小的边;Kruskal 算法将所有边按权重从小大到大排序,每次都选择权重最小且不会形成环的边。这两个贪心策略都被证明能够精确地求得最小生成树。
单源最短路径问题是从一个指定的源点出发,找到到图中所有其他顶点的最短路径。Dijkstra 算法是解决这个问题的经典算法(在边权为非负的情况下)。Dijkstra 算法的核心思想也是贪心:每次都从“未确定最短路径”的顶点中,选择一个离源点最近的顶点,并将其“确定”,然后用它来更新其他相邻顶点的距离。这个贪心策略被证明是正确的。
0-1 背包问题是给定一组物品,每种物品都有自己的重量和价值,在限定的总重量内,如何选择才能使得物品的总价值最高。每种物品都只有一件,只能选择放或者不放。对于这个问题,一个很自然的贪心策略是优先选择“性价比”最高的物品,即价值与重量比值最高的物品。但是,这个贪心策略无法保证得到全局最优解。例如,背包重量 10kg,物品 A 价值 60 元,重量 5kg(性价比 12 元/kg),物品 B 价值 80 元,重量 8kg(性价比 10 元/kg)。如果贪心选择,放入物品 A 后,背包剩余容量为 5kg,无法再放入物品 B,最终总价值为 60 元。但是最优选择是只选择物品 B,最终总价值为 80 元。从反例可以看出,贪心选择(选 A)并没有得到最优解(选 B)。0-1 背包问题的最优解需要使用动态规划来求解。
选择题:假设有一组字符 \(\{ g,h,i,j,k,l \}\),它们对应的频率分别为 \(8\%,14\%,17\%,20\%,23\%,18\%\)。请问以下哪个选项是字符 \(g,h,i,j,k,l\) 分别对应的一组哈夫曼编码?
A. g: 1100, h: 1101, i: 111, l: 10, k: 00, j: 01
B. g: 0000, h: 001, i: 010, l: 011, k: 10, j: 11
C. g: 111, h: 110, i: 101, l: 100, k: 01, j: 00
D. g: 110, h: 111, i: 101, l: 100, k: 0, j: 01
答案

例:CF1779C Least Prefix Sum
题意:给定 \(n\) 个数,每次修改可以让一个数乘以 \(-1\),求最少操作次数使得前 \(m\) 个数的和是所有前缀和中最小的。
数据范围:\(m \le n \le 200000\)
解题思路
首先转换题意:
若 \(i<m\),则前 \(i\) 个数的和等于前 \(m\) 个数的和减去第 \(i+1\) 到第 \(m\) 个数的和;
若 \(i>m\),则前 \(i\) 个数的和等于前 \(m\) 个数的和加上第 \(m+1\) 到第 \(i\) 个数的和。
所以题目等价于要求,在修改后,前 \(m\) 个数的所有后缀和都不大于 \(0\)(第 \(1 \sim m\) 个数的和可以大于 \(0\));第 \(m+1\) 到第 \(n\) 个数的所有前缀和都不小于 \(0\)
当前 \(m\) 个数的某个后缀和大于 \(0\) 时,显然这时我们应该去修改最大的正数;当第 \(m+1\) 到第 \(n\) 个数的某个前缀和小于 \(0\) 时,显然这时我们应该去修改最小的负数;以上操作可以通过大根堆与小根堆来维护当前的数。
参考代码
#include <cstdio>
#include <queue>
using namespace std;
typedef long long LL;
const int N = 200005;
int a[N];
int main()
{
int t;
scanf("%d", &t);
while (t--) {
int n, m;
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
}
int ans = 0;
LL sum = 0;
priority_queue<int> q1;
for (int i = m; i > 1; i--) {
q1.push(a[i]); sum += a[i];
if (sum > 0) {
ans++;
int x = q1.top();
sum -= x; q1.pop();
sum -= x; q1.push(-x);
}
}
sum = 0;
priority_queue<int, vector<int>, greater<int>> q2;
for (int i = m + 1; i <= n; i++) {
q2.push(a[i]); sum += a[i];
if (sum < 0) {
ans++;
int x = q2.top();
sum -= x; q2.pop();
sum -= x; q2.push(-x);
}
}
printf("%d\n", ans);
}
return 0;
}
例:P1080 [NOIP2012 提高组] 国王游戏
解题思路
假设我们现在已经有了一个排列顺序
大臣左手的数字为 \(A[1] \sim A[n]\),右手的数字为 \(B[1] \sim B[n]\),国王的数字为 \(A[0], B[0]\)
考虑交换位置为 \(i, i+1\) 两位大臣的位置
交换前:两人的奖赏分别为 \(\frac{1}{B[i]} \prod \limits_{j=0}^{i-1} A[j]\) 和 \(\frac{A[i]}{B[i+1]} \prod \limits_{j=0}^{i-1} A[j]\)
交换后:两人的奖赏分别为 \(\frac{1}{B[i+1]} \prod \limits_{j=0}^{i-1} A[j]\) 和 \(\frac{A[i+1]}{B[i]} \prod \limits_{j=0}^{i-1} A[j]\)
其余大臣的奖赏不变
提取公因式后,发现交换前为 \(\max(\frac{1}{B[i]}, \frac{A[i]}{B[i+1]})\),交换后则为 \(\max(\frac{1}{B[i+1]}, \frac{A[i+1]}{B[i]})\)
两式同乘 \(B[i]*B[i+1]\) 后,只要比较 \(\max(B[i+1], A[i]*B[i])\) 和 \(max(B[i], A[i+1]*B[i+1])\),等价于比较 \(A[i]*B[i]\) 和 \(A[i+1]*B[i+1]\)
当前者大于后者时,应当进行交换使得结果更优(获得奖赏最多的大臣,所获奖赏尽可能的少)
参考代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1005;
const int LEN = 5000;
struct Person {
int a, b;
bool operator<(const Person& other) const {
return max(other.b, a * b) < max(b, other.a * other.b);
}
};
Person p[N];
vector<int> ans, cur, tmp1, tmp2, tmp;
void print_bigint(vector<int>& v) {
int len = int(v.size()) - 1;
for (int i = len; i >= 0; --i) printf("%d", v[i]);
printf("\n");
}
vector<int> mul(vector<int>& a, vector<int>& b) {
vector<int> ret;
ret.clear();
int la = a.size(), lb = b.size();
ret.resize(la + lb);
for (int i = 0; i < la; ++i)
for (int j = 0; j < lb; ++j) {
ret[i + j] += a[i] * b[j];
if (ret[i + j] >= 10) {
ret[i + j + 1] += ret[i + j] / 10;
ret[i + j] %= 10;
}
}
int len = int(ret.size()) - 1;
while (len > 0 && ret[len] == 0) {
ret.pop_back();
--len;
}
return ret;
}
bool greater_eq(vector<int>& a, vector<int>& b, int last_dg) {
int la = a.size(), lb = b.size();
if (last_dg + lb < la && a[last_dg + lb]) return true;
for (int i = 0; i < lb; ++i) {
int x = a[last_dg + lb - i - 1];
int y = b[lb - i - 1];
if (x != y) return x > y;
}
return true;
}
vector<int> div(vector<int> a, vector<int>& b) {
int la = a.size(), lb = b.size();
if (la < lb) return vector<int>(1);
vector<int> res;
res.clear();
res.resize(la - lb + 1);
int cur = la - lb;
for (int i = cur; i >= 0; --i) {
while (greater_eq(a, b, i)) {
for (int j = 0; j < lb; ++j) {
a[i + j] -= b[j];
if (a[i + j] < 0) {
a[i + j] += 10;
--a[i + j + 1];
}
}
++res[i];
}
}
int len = int(res.size()) - 1;
while (len > 0 && res[len] == 0) {
res.pop_back();
--len;
}
return res;
}
vector<int> convert(int x) {
if (x == 0) return vector<int>(1);
vector<int> res;
while (x > 0) {
res.push_back(x % 10);
x /= 10;
}
return res;
}
bool bigint_greater(vector<int>& a, vector<int>& b) {
int la = a.size(), lb = b.size();
if (la != lb) return la > lb;
for (int i = la - 1; i >= 0; --i)
if (a[i] != b[i]) return a[i] > b[i];
return false;
}
int main()
{
int n;
scanf("%d", &n);
for (int i = 0; i <= n; ++i) {
scanf("%d%d", &p[i].a, &p[i].b);
}
sort(p + 1, p + n + 1);
cur = convert(p[0].a);
ans.resize(1);
for (int i = 1; i <= n; ++i) {
tmp1 = convert(p[i].a);
tmp2 = convert(p[i].b);
tmp = div(cur, tmp2);
if (bigint_greater(tmp, ans)) ans = tmp;
cur = mul(cur, tmp1);
}
print_bigint(ans);
return 0;
}
在运用贪心算法时,有时候贪心策略并不容易判断;而在有时候,虽然想到了某些贪心策略,但其正确性并不容易证明,需要大胆猜测!
例:P1248 加工生产调度
此题较难,其结论可以记一下。
如何贪心?
- 根据 \(A_i\) 升序?
- 根据 \(B_i\) 降序?
- 根据 \(A_i - B_i\) 升序?
- 前半段根据 \(A_i\) 升序,后半段根据 \(B_i\) 降序?
为了要使总的空闲时间最少,就要先加工 \(A_i\) 小的,最后加工 \(B_i\) 小的产品
贪心策略
先做 \(A_i < B_i\) 的产品,并将这些产品按 \(A_i\) 升序排序,之后再做 \(A_i \ge B_i\) 的产品,并将这些产品按 \(B_i\) 降序排序
参考代码
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 1005;
int sign(int x) {
return x > 0 ? 1 : (x == 0 ? 0 : -1);
}
struct Product {
int a, b, id;
bool operator<(const Product& other) const {
int s1 = sign(a - b), s2 = sign(other.a - other.b);
if (s1 != s2) return s1 < s2;
if (s1 < 0) return a < other.a;
else return b > other.b;
}
};
Product p[N];
int main()
{
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%d", &p[i].a); p[i].id = i;
}
for (int i = 1; i <= n; i++) scanf("%d", &p[i].b);
sort(p + 1, p + n + 1);
int time_a = 0, time_b = 0;
for (int i = 1; i <= n; i++) {
time_a += p[i].a;
time_b = max(time_a, time_b) + p[i].b;
}
printf("%d\n", time_b);
for (int i = 1; i <= n; i++) printf("%d%c", p[i].id, i == n ? '\n' : ' ');
return 0;
}
反悔贪心
例题:P3545 [POI2012] HUR-Warehouse Store
解题思路
当一个人来的时候,如果当前的库存还够,直接满足;如果当前的库存不够,直接忽略这个人吗?如果前边有人买的比他多,可以放弃原来的人,把这个人换进去。
因此需要知道之前满足了的人里,买的最多的是谁?这可以用一个大根堆来维护。
过程中会不会出现:弹出了 \(1\) 个人,能补进去 \(2\) 个人?这是不可能的,因为如果要补 \(1\) 个人,新补的肯定也比弹出的小,最多踢 \(1\) 补 \(1\) 答案不变。
参考代码
#include <cstdio>
#include <queue>
#include <utility>
using ll = long long;
const int N = 250005;
int a[N], b[N];
bool ok[N];
int main()
{
int n; scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
for (int i = 1; i <= n; i++) scanf("%d", &b[i]);
ll cur = 0;
int ans = 0;
std::priority_queue<std::pair<int, int>> q; // 购买量,第几天
for (int i = 1; i <= n; i++) {
cur += a[i];
if (cur >= b[i]) {
cur -= b[i]; ans++; q.push({b[i], i});
} else if (!q.empty() && q.top().first > b[i]) {
cur += q.top().first - b[i]; q.pop(); q.push({b[i], i});
}
}
while (!q.empty()) {
ok[q.top().second] = true; q.pop();
}
printf("%d\n", ans);
for (int i = 1; i <= n; i++)
if (ok[i]) printf("%d ", i);
return 0;
}
例题:P2209 [USACO13OPEN] Fuel Economy S
难点:不知道每一站加油加多少,少了怕不够,多了怕浪费。
解题思路
按与起点的距离对所有加油站排序。
无解的判断:第一个站距离起点超过了 \(B\) 或者中间有两个点距离超过了 \(G\),或者最后一个加油站到终点距离超过了 \(G\)。
考虑“退油”这个操作,前面买的油可以在任何地方以原价退款(等价于当初没买油),在消耗了油的时候才把价格算进来,那么加油的时候可以统一加满,这样就解决了怕加多浪费这个顾虑。
如果油箱里有多种油,开一段距离,希望它消耗怎样的油?显然应该优先消耗价格最低的。
到达一个加油站之后,如果油箱里还有一堆油,这时候又能新加油,可以发现,油箱里比当前加油站贵的油都没意义了。所以可以把没用过的比当前加油站贵的油都淘汰了,然后用新油装满。
可以用单调队列模拟这个过程,维护当前油箱里边有多少单位什么价格的油。每一次从队首弹出开到当前加油站所消耗的油,算价钱;从队尾淘汰掉比这个加油站贵的油,用这个加油站的油装满油箱。
参考代码
#include <cstdio>
#include <utility>
#include <algorithm>
#include <deque>
using ll = long long;
const int N = 50005;
std::pair<int, int> sta[N]; // 距离,价格
int main()
{
int n, g, b, d; scanf("%d%d%d%d", &n, &g, &b, &d);
for (int i = 1; i <= n; i++) {
int x, y; scanf("%d%d", &x, &y);
sta[i] = {x, y};
}
std::sort(sta + 1, sta + n + 1);
sta[n + 1] = {d, 0};
ll ans = 0;
std::deque<std::pair<int, int>> dq; // 价格,油量
dq.push_back({0, b});
int cur = 0, vol = b;
for (int i = 1; i <= n + 1; i++) {
// 开到加油站
while (cur != sta[i].first) {
if (dq.empty()) {
printf("-1\n"); return 0;
}
if (dq.front().second > sta[i].first - cur) { // 最便宜的油足够开到加油站
ans += 1ll * (sta[i].first - cur) * dq.front().first;
vol -= (sta[i].first - cur);
dq.front().second -= (sta[i].first - cur);
cur = sta[i].first;
} else { // 最便宜的油全用完
ans += 1ll * dq.front().second * dq.front().first;
vol -= dq.front().second;
cur += dq.front().second;
dq.pop_front();
}
}
// 淘汰更贵的油
while (!dq.empty() && dq.back().first > sta[i].second) {
vol -= dq.back().second;
dq.pop_back();
}
// 加满油
if (vol < g) {
dq.push_back({sta[i].second, g - vol});
vol = g;
}
}
printf("%lld\n", ans);
return 0;
}
习题:P1016 [NOIP1999 提高组] 旅行家的预算
同 P2209 [USACO13OPEN] Fuel Economy S,本题的数据大多是小数,注意精度问题。
参考代码
#include <cstdio>
#include <deque>
#include <cmath>
#include <algorithm>
using namespace std;
const int N = 10;
const double EPS = 1e-6;
struct Station {
double d, p;
bool operator<(const Station& other) const {
return d != other.d ? d < other.d : p < other.p;
}
};
Station s[N];
struct Oil {
double p, v;
};
int main()
{
double d1, c, d2, p;
int n;
scanf("%lf%lf%lf%lf%d", &d1, &c, &d2, &p, &n);
for (int i = 1; i <= n; i++) scanf("%lf%lf", &s[i].d, &s[i].p);
sort(s + 1, s + n + 1);
double cur = 0, ans = 0;
deque<Oil> dq;
dq.push_back({p, c});
bool flag = true;
for (int i = 1; i <= n; i++) {
if (s[i].d > d1 - EPS) break;
double need = (s[i].d - cur) / d2;
while (!dq.empty()) {
if (dq.front().v > need - EPS) {
ans += dq.front().p * need;
dq.front().v -= need; need = 0;
break;
} else {
ans += dq.front().p * dq.front().v;
dq.pop_front(); need -= dq.front().v;
}
}
if (need > EPS) {
flag = false;
break;
}
double vol = (s[i].d - cur) / d2;
cur = s[i].d;
while (!dq.empty() && dq.back().p > s[i].p + EPS) {
vol += dq.back().v; dq.pop_back();
}
dq.push_back({s[i].p, vol});
}
double need = (d1 - cur) / d2;
while (!dq.empty()) {
if (dq.front().v > need - EPS) {
ans += dq.front().p * need; need = 0;
break;
} else {
ans += dq.front().p * dq.front().v; need -= dq.front().v;
dq.pop_front();
}
}
if (need > EPS) flag = false;
if (!flag) printf("No Solution\n");
else printf("%.2f\n", ans);
return 0;
}

浙公网安备 33010602011771号