title: 贪心
tags: [贪心]
categories: [OI, 算法专题]

  • 贪心是OI中最常使用的思想,几乎所有题目都要用到。

  • 其核心思想在于根据当前状态,做出局部的最优决策,直至达成最优解。

  • 常见套路:
    1.交换论证:把任意方案改成贪心方案,答案不会变差。
    2.邻项交换:两个元素换位置,证明贪心顺序更优。
    3.数学不等式:列式比较两种策略优劣。
    4.反证法:假设最优不是贪心,推出矛盾。

  • 有些时候,我们需要用到以上的方法推导出贪心策略;而在一些场景下,我们需要“大胆猜结论”,即先通过观察得到一个对贪心策略的猜想,再尝试通过上述方法证明或证伪。

  • 而在模拟赛/正式比赛时,我们往往没有充足的时间用来对贪心策略进行严格证明。此时也可以通过写暴力对拍的方式验证正确性。

CF2182E New Year's Gifts

原题 · 标签:#贪心

考点

很典型的贪心思路

题意

  • Monocarp 有 \(n\) 个朋友、\(m\) 个盒子、\(k\) 枚硬币。
  • 第 \(i\) 个盒子的美观度为 \(a_i\),每个盒子最多装一个礼物。
  • 第 \(i\) 个朋友必须收到至少价值 \(y_i\) 的礼物。朋友在满足以下至少一个条件时会高兴:
    1. 礼物装在美观度 \(\ge x_i\) 的盒子里;
    2. 礼物价值 \(\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\)。