贪心
什么是贪心
在每次决策的时候都采取当前意义下的最优策略,一般贪心问题的难点在于最优策略的选择。
例题:有
懒狗策略(错误)
时间越长的工作越耽误时间,那么考虑按照工作时长排序,先选择做工作时间短的。
反例:(1, 4), (3, 5), (4, 7) 三个工作,按照这个策略会优先选择 (3, 5) 这个工作,从而剩下的工作都做不了了;而实际上最好的方案是做 (1, 4) 和 (4, 7) 这两个工作。
卷王策略(错误)
开始工作得越早,就能做更多的工作,考虑按照
反例:(1, 10), (3, 5), (7, 9) 三个工作,按照这个策略会优先选择 (1, 10) 这个工作,从而剩下的工作都做不了了;而实际上最好的方案是做 (3, 5) 和 (7, 9) 这两个工作。
正确策略
按照
证明:优先考虑结束时间可以在选择工作数量相同的情况下,为后续的选择提供更多的时间。
例题:CF1768C Elemental Decompress
题意:给定一个序列
,请构造两个排列 ,使得 ,可能无解。
数据范围:
解题思路
可以按照
比如
首先填第一个数和第五个数(
接着填第三个数,只需要一个
同理,让
重复从大到小的填数顺序,将
注意按以上策略填完数字后还需要重新检查一遍是否满足要求,不满足说明无解。
参考代码
#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. 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
题意:给定
个数,每次修改可以让一个数乘以 ,求最少操作次数使得前 个数的和是所有前缀和中最小的。
数据范围:
解题思路
首先转换题意:
若
若
所以题目等价于要求,在修改后,前
当前
参考代码
#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 提高组] 国王游戏
解题思路
假设我们现在已经有了一个排列顺序
大臣左手的数字为
考虑交换位置为
交换前:两人的奖赏分别为
交换后:两人的奖赏分别为
其余大臣的奖赏不变
提取公因式后,发现交换前为
两式同乘
当前者大于后者时,应当进行交换使得结果更优(获得奖赏最多的大臣,所获奖赏尽可能的少)
参考代码
#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 加工生产调度
此题较难,其结论可以记一下。
如何贪心?
- 根据
升序? - 根据
降序? - 根据
升序? - 前半段根据
升序,后半段根据 降序?
为了要使总的空闲时间最少,就要先加工
贪心策略
先做
参考代码
#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
解题思路
当一个人来的时候,如果当前的库存还够,直接满足;如果当前的库存不够,直接忽略这个人吗?如果前边有人买的比他多,可以放弃原来的人,把这个人换进去。
因此需要知道之前满足了的人里,买的最多的是谁?这可以用一个大根堆来维护。
过程中会不会出现:弹出了
参考代码
#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
难点:不知道每一站加油加多少,少了怕不够,多了怕浪费。
解题思路
按与起点的距离对所有加油站排序。
无解的判断:第一个站距离起点超过了
考虑“退油”这个操作,前面买的油可以在任何地方以原价退款(等价于当初没买油),在消耗了油的时候才把价格算进来,那么加油的时候可以统一加满,这样就解决了怕加多浪费这个顾虑。
如果油箱里有多种油,开一段距离,希望它消耗怎样的油?显然应该优先消耗价格最低的。
到达一个加油站之后,如果油箱里还有一堆油,这时候又能新加油,可以发现,油箱里比当前加油站贵的油都没意义了。所以可以把没用过的比当前加油站贵的油都淘汰了,然后用新油装满。
可以用单调队列模拟这个过程,维护当前油箱里边有多少单位什么价格的油。每一次从队首弹出开到当前加油站所消耗的油,算价钱;从队尾淘汰掉比这个加油站贵的油,用这个加油站的油装满油箱。
参考代码
#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; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?