贪心
前言
贪心常用的思考和证明方法:
- 反证法。
- 邻项交换。
- 数学归纳法。
最优子结构
对于一个子问题,其最优解由它的更小的子问题的解得到。
dp 子问题的最优解是由更小的若干子问题的解组合得到。
贪心子问题的最优解是由更小的子问题的最优解得到。
杂记
线段覆盖
给定 \(n\) 个线段,求最多选择对少线段是的所有线段没有重叠。
例题
P9925 [POI 2023/2024 R1] Zapobiegliwy student
P8860 动态图连通性
邻项交换
P2672 [NOIP 2015 普及组] 推销员
- 首先将 \(n\) 个住户按照疲劳值 \(a\) 从大到小排序。
- 对于枚举的 \(x\),最大疲劳值要么是前 \(x\) 个人,贡献是 \(2 \times \max \{ dis_i \} + \sum a_i\),或者是 \([x + 1, n]\) 中 \(a_i + 2 \times dis_i\) 中最大的。
P1842 [USACO05NOV] 奶牛玩杂技
- 本质上是从 \(1 \to n\) 的排列中找最优排列,考虑邻项交换。
- 假设有两头牛 \(x, y\),且 \(x, y\) 上面的牛总重量为 \(W\)。
- \(x\) 在 \(y\) 上:
\(x\) 压扁指数:\(W - s_x\)。
\(y\) 压扁指数:\(W + w_x - s_y\)。
答案为:\(\max \{ W - s_x, W + w_x - s_y \}\)。
- \(y\) 在 \(x\) 上:
\(y\) 压扁指数:\(W - s_y\)。
\(x\) 压扁指数:\(W + w_y - s_x\)。
答案为:\(\max \{ W - s_y, W + w_y - s_x \}\)。
令 \(x\) 在上面更优,则:
\[\max \{ W - s_x, W + w_x - s_y \} \le \max \{ W - s_y, W + w_y - s_x \}
\]
因为 \(w_i\) 为正,所以 \(W - s_x < W + w_y - s_x\)。
进而 \(W + w_x - s_y \le W + w_y - s_x\)。
移项得:\(w_x + s_x \le w_y + s_y\)。
P1080 [NOIP 2012 提高组] 国王游戏
反悔贪心
多用于决策较少时。
CF865D Buy Low Sell High
P1484 种树
P2949 [USACO09OPEN] Work Scheduling G
P3620 [APIO/CTSC2007] 数据备份
P4053 [JSOI2007] 建筑抢修
考虑按 \(T_2\) 排序,则反悔时尽量选 \(T_1\) 大的弹出即可。
代码:
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 15e4 + 5;
int n;
struct Node {
int t1, t2;
} a[N];
priority_queue<int> q;
signed main() {
ios_base :: sync_with_stdio(NULL);
cin.tie(nullptr);
cout.tie(nullptr);
cin >> n;
for(int i = 1 ; i <= n ; ++ i)
cin >> a[i].t1 >> a[i].t2;
sort(a + 1, a + 1 + n, [&](Node a, Node b) {
return a.t2 < b.t2;
});
int ans = 0, sum = 0;
for(int i = 1 ; i <= n ; ++ i) {
sum += a[i].t1;
q.push(a[i].t1);
if(sum > a[i].t2) {
sum -= q.top();
q.pop();
}
else ++ ans;
}
cout << ans;
return 0;
}
CF730I Olympiad in Programming and Sports
- 每个人的选择只有 \(3\) 种,考虑反悔贪心。
- 设先组编程队,维护一个大根堆 \(q1\) 存储 \(n\) 个人的编程能力。
- 先取 \(p\) 个人组编程队,维护 \(ans\) 对编程能力求和。
- 组编程队时,要建立反悔机制,且只能反悔到运动队。因此再维护一个大根堆 \(q3\) 存需要反悔的人运动与编程能力的差值。
- 当 \(p\) 个人已经组队且建立了反悔机制,然后尝试组 \(s\) 个人到运动队。
- 另按运动能力维护一个大根堆 \(q2\),\(n\) 个人都进堆。
- 循环 \(s\) 次,尝试组运动队,要么取 \(q2\) 的堆顶,要么取 \(q1\) 和 \(q3\) 的堆顶之和。
#include <bits/stdc++.h>
#define int long long
#define fi first
#define se second
using namespace std;
const int N = 3e3 + 5;
int n, p, s, a[N], b[N], vis[N];
priority_queue<pair<int, int> > q1, q2, q3;
signed main() {
ios_base :: sync_with_stdio(NULL);
cin.tie(nullptr);
cout.tie(nullptr);
cin >> n >> p >> s;
for(int i = 1 ; i <= n ; ++ i) {
cin >> a[i];
q1.push({a[i], i});
}
for(int i = 1 ; i <= n ; ++ i) {
cin >> b[i];
q2.push({b[i], i});
}
int ans = 0;
for(int i = 1 ; i <= p ; ++ i) {
int id1 = q1.top().se;
q1.pop();
vis[id1] = 1;
ans += a[id1];
q3.push({b[id1] - a[id1], id1});
}
for(int i = 1 ; i <= s ; ++ i) {
while(! q1.empty() && vis[q1.top().se]) q1.pop();
while(! q2.empty() && vis[q2.top().se]) q2.pop();
int id1 = q1.top().se, id2 = q2.top().se, id3 = q3.top().se;
if(b[id2] > a[id1] + b[id3] - a[id3]) {
q2.pop();
vis[id2] = 2;
ans += b[id2];
}
else {
q1.pop(), q3.pop();
vis[id1] = 1, vis[id3] = 2;
ans += a[id1] + b[id3] - a[id3];
q3.push({b[id1] - a[id1], id1});
}
}
cout << ans << '\n';
for(int i = 1 ; i <= n ; ++ i)
if(vis[i] == 1) cout << i << ' ';
cout << '\n';
for(int i = 1 ; i <= n ; ++ i)
if(vis[i] == 2) cout << i << ' ';
return 0;
}
P3045 [USACO12FEB] Cow Coupons G
AT_agc018_c [AGC018C] Coins
方法 \(1\)(普通反悔贪心):
- 准备 \(3\) 个大根堆 \(q1, q2, q3\),维护金银铜的最多币数。
- 优先组金币集合,并建立金到银、铜的反悔机制,用 \(q4, q5\) 两个堆维护。
- 在组银币集合,并建立银到铜的反悔机制,用 \(q6\) 堆维护。
方法 \(2\)(利用 \(3\) 个集合不独立转化为 \(2\) 个集合):
- 假设所有人都进金币集合。
- 同时维护金到银的反悔堆,再维护金到铜的反悔堆。
- 稍微推一下式子,不难发现如果按银和铜的价值差值排序后,反悔银的一定是一段前缀,反悔铜的一定是一段后缀。
CF436E Cardboard Box
方法 \(1\):
直接贪。
方法 \(2\):
- 将第 \(i\) 个关卡转化为有两颗星星,第一颗代价为 \(a_i\),第二颗代价为 \(b_i - a_i\)。
- 维护 \(a_i\) 的小根堆,先选 \(w\) 颗一颗的星星。
- 反悔时可能会出现选两颗星星代价为 \(b_j\),但 \(a_j\) 根本不在堆中,没有反悔机会。
- 反悔的本质是 \(2\) 个关卡的一颗星星换 \(1\) 个关卡的 \(2\) 颗星星,则在维护一颗星星的堆取两个有效值与维护两颗星星的堆的堆顶取较小值即可。
方法 \(3\):
将选两颗星星优于选一颗星星的关卡放在集合 \(A\) 里,其余放在集合 \(B\) 里,类似于双指针去贪心即可。