title: 贪心
tags: [贪心]
categories: [OI, 算法专题]
-
贪心是OI中最常使用的思想,几乎所有题目都要用到。
-
其核心思想在于根据当前状态,做出局部的最优决策,直至达成最优解。
-
常见套路:
1.交换论证:把任意方案改成贪心方案,答案不会变差。
2.邻项交换:两个元素换位置,证明贪心顺序更优。
3.数学不等式:列式比较两种策略优劣。
4.反证法:假设最优不是贪心,推出矛盾。 -
有些时候,我们需要用到以上的方法推导出贪心策略;而在一些场景下,我们需要“大胆猜结论”,即先通过观察得到一个对贪心策略的猜想,再尝试通过上述方法证明或证伪。
-
而在模拟赛/正式比赛时,我们往往没有充足的时间用来对贪心策略进行严格证明。此时也可以通过写暴力对拍的方式验证正确性。
CF2182E New Year's Gifts
原题 · 标签:#贪心
考点
很典型的贪心思路
题意
- Monocarp 有 \(n\) 个朋友、\(m\) 个盒子、\(k\) 枚硬币。
- 第 \(i\) 个盒子的美观度为 \(a_i\),每个盒子最多装一个礼物。
- 第 \(i\) 个朋友必须收到至少价值 \(y_i\) 的礼物。朋友在满足以下至少一个条件时会高兴:
- 礼物装在美观度 \(\ge x_i\) 的盒子里;
- 礼物价值 \(\ge z_i\)(\(z_i > y_i\))。
- 目标:用不超过 \(k\) 枚硬币,使高兴的朋友数最大化。
- 保证 \(\sum y_i \le k\)(足够买每人至少 \(y_i\) 的礼物)。
输入格式:
- 第一行 \(t\),测试用例数。
- 每组数据:第一行 \(n, m, k\);第二行 \(m\) 个整数 \(a_i\);接下来 \(n\) 行每行三个整数 \(x_i, y_i, z_i\)。
输出格式:
- 每组数据输出一行,表示最大高兴人数。
数据范围:
- \(1 \le n, m \le 2 \times 10^5\),\(1 \le k \le 10^{15}\)
- \(1 \le a_i \le m\),\(1 \le x_i \le m\),\(1 \le y_i < z_i \le 10^9\)
- \(\sum y_i \le k\)
- 所有测试用例 \(\sum n \le 2 \times 10^5\),\(\sum m \le 2 \times 10^5\)
思路
首先,每个人都必须花费 \(y_i\) ,都花费相当于都没花费,最后会剩余 \(k - \sum y_i\) ,这一部分才是可以支配的使朋友高兴的部分。
让朋友高兴要么用 \(z_i - y_i\) 枚硬币要么用盒子。先处理盒子。在朋友要这个盒子的范围内,这个朋友需要的硬币数量越多,我用盒子来填硬币的窟窿就越大。也就是说,在盒子能送给的朋友的范围内,选择硬币差值最大的那个。
盒子选完后就只能用硬币了。这儿就很显然了:选小的硬币差值,直到不够选了。
代码
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define f(i,a,b) for(int i=(a);i<=(b);i++)
#define Dl(a) cout << #a << " : " << a << "\n";
#define D(a) cout << #a << " : " << a;
#define Da(a,i,j) cout << #a << " : ";f(idx,i,j){cout << a[idx] <<" ";}
int t;
int n,m,k;
int ans;
struct st{
int x,y,z;
};
vector <int> a;
vector <st> s;
priority_queue <int> q;
void init(){
while (!q.empty()) q.pop();
ans = 0;
a.clear(),s.clear();
a.resize(m+10),s.resize(n+10);
}
bool cmp(st a,st b){
return a.x < b.x;
}
signed main(){
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
cin >> t;
while (t--){
cin >> n >> m >> k;
init();
f(i,1,m){
cin >> a[i];
}
int sigmaY = 0;
f(i,1,n){
cin >> s[i].x >> s[i].y >> s[i].z;
sigmaY += s[i].y;
}
k -= sigmaY;
sort(a.begin()+1,a.begin()+1+m);
sort(s.begin()+1,s.begin()+1+n,cmp);
int idx = 1;
f(i,1,m){
// 更新堆内元素
while (idx <= n && s[idx].x <= a[i]){
q.push(s[idx].z - s[idx].y);
idx++;
}
if (q.empty()) continue;
// D(q.top());
q.pop();
ans ++;
}
f(i,idx,n){
q.push(s[i].z - s[i].y);
}
stack <int> stk;
while (!q.empty()){
stk.push(q.top());
q.pop();
}
while (!stk.empty()&&k - stk.top() >= 0){
k -= stk.top();
stk.pop();
ans++;
}
cout << ans << "\n";
}
return 0;
}
P7514 省选联考 2021 AB 卷 卡牌游戏
原题 · 标签:#贪心 #双指针
考点
我秒的很快的一道题,很惊人
题意
- Alice 有 \(n\) 张卡牌,初始全部正面朝上。第 \(i\) 张正面数字为 \(a_i\),背面为 \(b_i\),所有 \(2n\) 个数字互不相同,且 \(a_i\) 已升序给出。
- 可以选择不超过 \(m\) 张翻面,目标是使最终朝上的 \(n\) 个数字的极差(最大值 \(-\) 最小值)最小。
- 求这个最小极差。
输入格式:
- 第一行 \(n, m\)。
- 第二行 \(n\) 个整数 \(a_i\)。
- 第三行 \(n\) 个整数 \(b_i\)。
输出格式:
- 一行一个整数,表示最小极差。
数据范围:
- \(3 \le n \le 10^6\),\(1 \le m < n\),\(1 \le a_i, b_i \le 10^9\)。
- 所有 \(2n\) 个数字互不相同,\(a_i\) 升序给出。
思路
这题课上讲的,我怎么秒的这么快(被自己吓到
极差最小,那就排序。排序后的左端点就是最小值,右端点就是最大值,它们的差就是极差嘛。它们组成一个窗口,这个窗口近可能小,那它的极差就尽可能小。
既然如此,先混合排序。慢慢推进左端点,移动右端点。移动完后得维护卡牌的数量和翻面的个数。这个特判就好。感觉自我代码良好。
代码
#include <bits/stdc++.h>
using namespace std;
//#define int long long
#define f(i,a,b) for(int i=(a);i<=(b);i++)
#define Dl(a) cout << #a << " : " << a << "\n";
#define D(a) cout << #a << " : " << a;
#define Da(a,i,j) cout << #a << " : ";f(idx,i,j){cout << a[idx] <<" ";}
const int N = 2e6+10;
struct st{
int num,state,idx;
// 0 正面 1反面
}s[N];
int ans;
int vis[N];
/*
无正无反:0
有正无反:1
无正有反:2
有正有反:3
*/
int n,m;
bool cmp(st a,st b){
return a.num < b.num;
}
signed main(){
ans = 0x3f3f3f3f;
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
cin >> n >> m;
f(i,1,n){
cin >> s[i].num;
s[i].idx = i;
s[i].state = 0;
}
f(i,1,n){
cin >> s[i+n].num;
s[i+n].idx = i;
s[i+n].state = 1;
}
sort(s+1,s+1+n+n,cmp);
// D(s[1].num);
int rig = 0;
int nCnt = 0,mCnt = 0;
// 区间范围:[lef,rig]
f(lef,1,n+n){
while (rig + 1 <= n+n && (nCnt < n || mCnt > m)){
rig++;
int cardIdx = s[rig].idx,cardState = s[rig].state;
// D(cardState);
int oldState = vis[cardIdx];
int newState = oldState + ((cardState) ? 2 : 1);
// 更新mCnt与nCnt
if (oldState == 0){
nCnt++;
}
if (oldState == 0 && newState == 2){
mCnt++;
}
if (oldState == 2 && newState == 3){
mCnt--;
}
vis[cardIdx] = newState;
}
// D(nCnt);
// Dl(mCnt);
if (nCnt == n && mCnt <= m){
ans = min(ans,s[rig].num - s[lef].num);
}
// 更新左端点++后
int cardIdx = s[lef].idx,cardState = s[lef].state;
int oldState = vis[cardIdx];
int newState = oldState - ((cardState) ? 2 : 1);
if (newState == 0){
nCnt--;
}
if (oldState == 2 && newState == 0){
mCnt--;
}
if (oldState == 3 && newState == 2){
mCnt++;
}
vis[cardIdx] = newState;
}
cout << ans;
return 0;
}
P2107 小 Z 的 AK 计划
原题 · 标签:#贪心
考点
反悔贪心的本质是待选序列
题意
- 一条街上有 \(n\) 个机房,第 \(i\) 个机房坐标为 \(x_i\),小 Z 从坐标 \(0\) 的家出发,移动速度为 \(1\)。
- 到达第 \(i\) 个机房后,可选择花 \(t_i\) 时间想题并瞬间 AK,或直接路过。
- 总可用时间为 \(m\),求最多能在多少个机房 AK。
输入格式:
- 第一行 \(n, m\)。
- 接下来 \(n\) 行,每行 \(x_i, t_i\)。
输出格式:
- 一行一个整数,表示最多 AK 的机房数量。
数据范围:
- \(1 \le n \le 10^5\),\(0 \le m, x_i \le 10^{18}\),\(0 \le t_i \le 10^9\)。
思路
走路不走回头路,所以到一个地方能刷题的时间是总时间减去其所在的 \(x\) 。
然后用一个优先队列做反悔贪心,实际上是当做待选序列。
代码
#include <bits/stdc++.h>
using namespace std;
//#define int long long
#define f(i,a,b) for(int i=(a);i<=(b);i++)
#define Dl(a) cout << #a << " : " << a << "\n";
#define D(a) cout << #a << " : " << a;
#define Da(a,i,j) cout << #a << " : ";f(idx,i,j){cout << a[idx] <<" ";}
const int N = 2e6+10;
struct st{
int num,state,idx;
// 0 正面 1反面
}s[N];
int ans;
int vis[N];
/*
无正无反:0
有正无反:1
无正有反:2
有正有反:3
*/
int n,m;
bool cmp(st a,st b){
return a.num < b.num;
}
signed main(){
ans = 0x3f3f3f3f;
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
cin >> n >> m;
f(i,1,n){
cin >> s[i].num;
s[i].idx = i;
s[i].state = 0;
}
f(i,1,n){
cin >> s[i+n].num;
s[i+n].idx = i;
s[i+n].state = 1;
}
sort(s+1,s+1+n+n,cmp);
// D(s[1].num);
int rig = 0;
int nCnt = 0,mCnt = 0;
// 区间范围:[lef,rig]
f(lef,1,n+n){
while (rig + 1 <= n+n && (nCnt < n || mCnt > m)){
rig++;
int cardIdx = s[rig].idx,cardState = s[rig].state;
// D(cardState);
int oldState = vis[cardIdx];
int newState = oldState + ((cardState) ? 2 : 1);
// 更新mCnt与nCnt
if (oldState == 0){
nCnt++;
}
if (oldState == 0 && newState == 2){
mCnt++;
}
if (oldState == 2 && newState == 3){
mCnt--;
}
vis[cardIdx] = newState;
}
// D(nCnt);
// Dl(mCnt);
if (nCnt == n && mCnt <= m){
ans = min(ans,s[rig].num - s[lef].num);
}
// 更新左端点++后
int cardIdx = s[lef].idx,cardState = s[lef].state;
int oldState = vis[cardIdx];
int newState = oldState - ((cardState) ? 2 : 1);
if (newState == 0){
nCnt--;
}
if (oldState == 2 && newState == 0){
mCnt--;
}
if (oldState == 3 && newState == 2){
mCnt++;
}
vis[cardIdx] = newState;
}
cout << ans;
return 0;
}
P14333 JOI2021 预选赛 R2 安全检查 Safety Inspection
原题 · 标签:#贪心
考点
很经典的套路,第二次出现了诶
题意
数轴上有 \(N\) 个按坐标升序排列的设施,每个设施需完成 \(B_i\) 项检查。共 \(K\) 名工人,初始位于坐标 \(0\),每人每分钟可移动1单位距离或完成1项检查。
求完成所有检查的最短时间。
数据范围
- \(1 \le N \le 10^5\)
- \(1 \le K,A_i,B_i \le 10^9\)
- 保证 \(A_1<A_2<\dots<A_N\),所有输入均为整数。
思路
发现可以从路途最远的项目上开始考虑。
可以将题目理解为使工人工作时间的最大值最小,所以二分时间。
考虑在路途中的时间没有做工,也就是说从路途大的工作项目开始考虑,工作的人数应尽可能小,才可以使在路途上的时间最少。但是又不能使在二分的时间内做不完。
这样也可能会浪费工时,因为可能做完后工人就歇着了。所以取模,剩下的工时给下一大的项目用。
代码
#include <bits/stdc++.h>
using namespace std;
#define inf 0x3f3f3f3f
#define f(i,a,b) for(int i=(a);i<=(b);i++)
#define int long long
#define pii pair<int,int>
#define DEBUG 1
#if DEBUG
#define d(x) cout << #x << " = " << x << endl;
#define da(a,l,r) cout << #a << "[" << l << "-" << r << "]:"; f(i,l,r) cout << a[i] << " "; cout << endl;
#else
#define d(x)
#define da(a,l,r)
#endif
const int N = 2e5+10;
int n,k;
int a[N],b[N];
bool check(int mid){
int cnt = 0,ans = 0;
for (int i = n;i >= 1;i--){
if (cnt >= b[i]){
cnt -= b[i];
}
else{
int x = (b[i] - cnt + (mid - a[i] - 1)) / (mid - a[i]);
cnt = x * (mid - a[i]) - (b[i] - cnt);
ans += x;
}
}
return ans <= k;
}
signed main() {
cin >> n >> k;
f(i,1,n){
cin >> a[i];
}
f(i,1,n){
cin >> b[i];
}
int l = a[n] + 1,r = 1e16,ans = -1;
while (l <= r){
int mid = (l + r) / 2;
if (check(mid)){
r = mid - 1;
ans = mid;
}
else{
l = mid + 1;
}
}
cout << ans;
return 0;
}
P8903 USACO22DEC Bribing Friends G
原题 · 标签:#DP
考点
很好的思路题
题意
- Bessie 有 \(N\) 个朋友、\(A\) 哞尼、\(B\) 个冰激凌甜筒。
- 第 \(i\) 个朋友有受欢迎度 \(P_i\),需要至少 \(C_i\) 哞尼才愿意陪她。可以用冰激凌甜筒换取折扣:每 \(X_i\) 个甜筒减免 \(1\) 哞尼(不能倒找钱)。
- 目标:在不超过 \(A\) 哞尼和 \(B\) 甜筒的前提下,最大化陪她的朋友的受欢迎度之和。
输入格式:
- 第一行 \(N, A, B\)。
- 接下来 \(N\) 行,每行 \(P_i, C_i, X_i\)。
输出格式:
- 一行一个整数,表示最大受欢迎度之和。
数据范围:
- \(1 \le N \le 2000\),\(0 \le A, B \le 2000\),\(1 \le P_i, C_i, X_i \le 2000\)。
思路
按照 \(X_i\) 排序,显然前面的部分要用甜筒,后面的部分要用钱。中间这一个点混合用。
分开算前面只用甜筒的和后面只用钱的01背包。
枚举中间点,枚举用甜筒换多少钱。
注意DP的边界和继承!别复制完之后没改!
代码
#include <bits/stdc++.h>
using namespace std;
//#define int long long
#define f(i,a,b) for(int i=(a);i<=(b);i++)
#define Dl(a) cout << #a << " : " << a << "\n";
#define D(a) cout << #a << " : " << a;
#define Da(a,i,j) cout << #a << " : ";f(idx,i,j){cout << a[idx] <<" ";}
const int N = 2e3+10;
int dp1[N][N],dp2[N][N];
struct st{
int p,c,x;
} s[N];
bool cmp(st a,st b){
return a.x < b.x;
}
int n,a,b;
int ans;
signed main(){
ans = 0;
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
cin >> n >> a >> b;
f(i,1,n){
cin >> s[i].p >> s[i].c >> s[i].x;
}
sort(s+1,s+1+n,cmp);
f(i,1,n){
for (int j=b;j>=0;j--){
dp1[i][j] = dp1[i-1][j];
}
for (int j = b;j>=s[i].c * s[i].x;j--){
dp1[i][j] = max(dp1[i][j],dp1[i-1][j-s[i].x * s[i].c] + s[i].p);
}
}
for (int i=n;i>=1;i--){
for (int j=a;j>=0;j--){
dp2[i][j] = dp2[i+1][j];
}
for (int j=a;j>=s[i].c;j--){
dp2[i][j] = max(dp2[i][j],dp2[i+1][j-s[i].c] + s[i].p);
}
}
f(i,1,n){
for (int j=0;j * s[i].x<=b && j<=s[i].c;j++){ // 甜筒换的钱
ans = max(ans,dp1[i-1][b-j*s[i].x] + s[i].p + dp2[i+1][a-s[i].c+j]);
// D(i);
// D(dp1[i-1][b-j*s[i].x]);D(s[i].p);Dl(dp2[i+1][a-s[i].c+j]);
}
ans = max(ans,dp1[i-1][b] + dp2[i+1][a]);
}
cout << ans;
return 0;
}
P11328 NOISG 2022 Finals Gym Badges
原题 · 标签:#TODO
题意
- 初始等级为 \(0\)。有 \(N\) 个比赛,第 \(i\) 个比赛要求当前等级 \(\le L_i\) 才能参加,参加后等级增加 \(X_i\),并获得一个徽章。
- 可以任意顺序参加比赛,求最多能获得的徽章数。
数据范围:\(1 \le N \le 5 \times 10^5\),\(1 \le X_i, L_i \le 10^9\)。
浙公网安备 33010602011771号