title: 专题1 - 贪心、二分
tags: [贪心, 二分]
categories: [OI, 算法专题]

二分

二分是很基础的知识了,在这里我还是不完全按照题目的顺序,一点一点来梳理一下二分。
二分,根本含义是利用单调性,寻找取值变化的改变点。一般来说,我们可以把可以使用二分解决的问题建模为0/1序列,这个序列具有单调性。通过二分过程中代表的区间[left,right]的不断缩小,来接近这个点。
为什么要用二分?因为二分可以在几乎没有预处理的情况下,单靠check函数,来最快的逼近答案。

整数二分模板

尽可能取大值:

int left = min,right = max,ans = defaultValue;
while (left <= right){
	int mid = left + (right - left) / 2;
	if (check(mid)){
		left = mid + 1;
		ans = mid;
	}
	else{
		right = mid - 1;
	}
}

尽可能取小值:

int left = min,right = max,ans = defaultValue;
while (left <= right){
	int mid = left + (right - left) / 2;
	if (check(mid)){
		right = mid - 1;
		ans = mid;
	}
	else{
		left = mid + 1;
	}
}

check(ans)为true。也就是说,其得到的结果是闭区间。
这个模板是我最适应的。一开始我对边界的划分很迷,这样写感觉很舒服,对边界的划分比较无脑。

实数二分模板

尽可能取大值:

double left = min,right = max,ans = defaultValue;
while ((right - left) >= eps){
	double mid = (left + right) /2 ;
	if (check(mid)){
		left = mid;
		ans = mid;
	}
	else{
		right = mid;
	}
}

尽可能取小值:

double left = min,right = max,ans = defaultValue;
while ((right - left) >= eps){
	double mid = (left + right) /2 ;
	if (check(mid)){
		right = mid;
		ans = mid;
	}
	else{
		left = mid;
	}
}

诶,发现大同小异。

贪心

贪心的思考从下面几个方面考虑:
1.在工时固定的情况下,使得更多的时间做工
2.利用单调性,不走无用功
3.分解答案,将复杂构成的答案变得简单

习题

P13932 蓝桥杯 2022 省 Java B 求阶乘

原题 · 标签:#二分 #数学

考点

发现性质后简单二分

题意

满足 \(N!\) 的末尾恰好有 \(K\) 个 \(0\) 的最小的 \(N\) 是多少?

如果这样的 \(N\) 不存在输出 \(-1\)。

思路

发现如果把\(N!\)质因数分解,则\(5\)的个数一定小于\(2\)的个数。也就是说,末尾\(0\)的数量取决于\(5\)的个数。
那么就二分,得到最小的\(N\)使得其阶乘的\(5\)的因子个数大于\(K\)。

代码

#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
int k;
int check(int x){
	int res = 0;
	while (x){
		x /= 5;
		res += x;
	}
	return res;
}
signed main() {
	cin >> k;
	int l = 0,r=5e18,ans = -1;
	while (l <= r){
		int mid = (l + r) / 2;
		if (check(mid) >= k){
			ans = mid;
			r = mid - 1;
		}
		else{
			l = mid + 1;
		}
	}
	if (check(ans) == k) cout << ans;
	else cout << -1;
	return 0;
}

P4105 HEOI2014 南园满地堆轻絮

原题 · 标签:#二分 #贪心

考点

最小值最大经典习题+贪心check

题意

  1. 按给定递推公式生成长度为 \(n\) 的整数序列 \(A\)。
  2. 构造非递减序列 \(B\),要求最小化 \(\max|A_i-B_i|\),输出这个最小的最大值。

思路

最小值最大,就二分。
由于要求非递减,则在要求的限制下越小越有利于之后的发展。
所以顺着来,如果本来就可以,就取减去最小值与本来的数的max;如果不可以,不能修改挽回就返回false,否则原数加上最小值与要求取min。

代码

#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 = 5e6+10;
int n,sa,sb,sc,sd,a1,mod;
int a[N],b[N];
int fac(int x){
	return ((sa * x%mod * x )%mod * x%mod + sb * x %mod* x%mod + sc * x%mod + sd) % mod;
}
void init(){
	a[1] = a1;
	f(i,2,n){
		a[i] = (fac(a[i-1]) %mod+ fac(a[i-2])%mod) % mod;	
	}
}
bool check(int x){
	memset(b,0,sizeof b);
	f(i,1,n){
		// 本来就行
		if (a[i] >= b[i-1]){
			b[i] = max(b[i-1],a[i]-x);
		}
		// 得改
		else{
			// 改不了
			if (abs(b[i-1] - a[i]) > x){
				return false;
			}
			// 能改
			b[i] = min(b[i-1],a[i] + x);
		}
	}
	return true;
}
signed main() {
	cin >> n >> sa >> sb >> sc >> sd >> a1 >> mod;
	init();
//	da(a,1,n);
	int l = 0,r = 2e9,ans;
	while (l <= r){
		int mid = (l + r) / 2;
//		d(mid);
		if (check(mid)){
//			d(mid);
			r = mid - 1;
			ans = mid;
		}
		else{
			l = mid + 1;
		}
	}
	cout << ans;
	return 0;
}

P6093 JSOI2015 套娃

原题 · 标签:#贪心 #std

考点

贪心+std

思路

TIP1

答案可以分解为不套娃的答案 \(-\) 套娃产生的贡献。

TIP2

不套娃的答案是 \(\sum_{i=1}^{n} In_i \times B_i\)。

若把 \(i\) 号套娃放在 \(j\) 号套娃里面,则会使答案减少 \(out_i \times b_j\)。

TIP3

从大套娃的角度考虑。如果有多个大套娃,有资格竞争同个小套娃,那就是 \(b\) 大者得。

从小套娃的角度考虑,如果有多个小套娃,有资格竞争同个大套娃,那就是 \(out\) 大者得。

你说的我都懂,咋实现啊?

multiset 我咋以前没听说过

思路历程

发现好像并不是娃都能套上,甚至套上的娃比较少,那能不能从套娃这个行为对答案的贡献出发?

想了想,不套娃的贡献很好算,套娃就是上述分开想一想,然后贪心。

然后就发现复杂度应该能降,但我不会了

看题解发现了 multiset 这个东西,set 这个东西我几乎不用。对于要处理数组处理不到的东西就用 map 或者 unordered_map,当加强版数组用。现在发现 std 还真是个好东西。

代码

代码

#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;
struct st{
	int out,in,b;
};
bool cmp(st a,st b){
	return a.b > b.b;
}
st a[N];
multiset <int> s;
int n;
int ans;
signed main() {
	cin >> n;
	f(i,1,n){
		cin >> a[i].out >> a[i].in >> a[i].b;
		ans += a[i].in * a[i].b;
		s.insert(a[i].out);
	}
	sort(a+1,a+1+n,cmp);
	f(i,1,n){
		auto it = s.lower_bound(a[i].in);
		if (it != s.begin()){
			ans -= (*--it)*a[i].b;
			s.erase(it);
		}
	}
	cout << ans;
	return 0;
}

P14333 JOI2021 预选赛 R2 安全检查 Safety Inspection

原题 · 标签:#贪心

考点

抽离二分+贪心check

题意

数轴上有 \(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;
}