二分

二分

二分一般再应用时分为整数二分和实数二分。整数域上的二分要注意边界条件、左右区间的开闭情况,避免遗漏答案或者陷入死循环;实数域上的二分要注意精度问题。

二分答案

题目的答案具备单调性,可以在某个区间里利用二分求解

注意最终答案的范围,需要仔细确定一下!!!

整数二分

两个经典模型:最大值最小化 和 最小值最大化

最大值最小化

题意
给定 n 个顶点,m条边的无向图,每个点都有点权 \(f_i\),边均有边权。求起点 1 到终点 N 的所有可能路径中,在路径总边权不超过 b 的情况下,路径上最大点权(路径上的点中的最大点权)的最小值

思路
可以对点权进行二分,之后判断仅利用不大于给定点权的点跑最短路是否符合(如果最短路的总边权都大于 b,那么其他路径也都大于 b)

“仅利用不大于给定点权的点跑最短路” 即为简单,在跑最短路的时候加一个判断,下一个相邻点点权大于给定点权则直接跳过这个相邻点。别忘了起点的点权也要算在里面!

不存在相关路径更简单,将给定点权设置为足够大跑最短路,判断这时是否成立即可

剩下的就是最短路的事情了,详见代码
链式前向星存无向图不开双倍空间,RE 的就是你!!!

代码

//>>>Qiansui
#include<bits/stdc++.h>
#define ll long long
#define ull unsigned long long
#define mem(x,y) memset(x, y, sizeof(x))
#define debug(x) cout << #x << " = " << x << '\n'
#define debug2(x,y) cout << #x << " = " << x << " " << #y << " = "<< y << '\n'
//#define int long long

using namespace std;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
typedef pair<ull, ull> pull;
typedef pair<double, double> pdd;
/*
二分 + dij 最短路
*/
const int maxm = 1e4 + 5, mod = 998244353, maxn = 5e4 + 5;
const ll inf = 1e14;
int n, m, b, cnt = 1, head[maxm];
vector<int> f(maxm);

struct edge{
	int to, w, next;
}p[maxn << 1];

void add_edge(int u, int v, int w){
	p[cnt].to = v;
	p[cnt].next = head[u];
	p[cnt].w = w;
	head[u] = cnt ++;
	return ;
}

bool check(int ff){
	priority_queue<pll, vector<pll>, greater<pll>> q;
	vector<bool> vis(n + 1, false);
	vector<ll> dis(n + 1, inf);
	q.push({0, 1});
	pll t;
	while(!q.empty()){
		t = q.top(); q.pop();
		if(vis[t.second]) continue;
		vis[t.second] = true;
		dis[t.second] = t.first;
		for(int i = head[t.second]; i; i = p[i].next){
			int v = p[i].to;
			if(f[v] > ff) continue;
			if(!vis[v] && dis[v] > dis[t.second] + p[i].w){
				q.push({dis[t.second] + p[i].w, v});
			}
		}
	}
	if(dis[n] > b) return false;
	else return true;
}

void solve(){
	cin >> n >> m >> b;
	for(int i = 1; i <= n; ++ i){
		cin >> f[i];
	}
	for(int i = 0; i < m; ++ i){
		int x, y, c;
		cin >> x >> y >> c;
		add_edge(x, y, c);
		add_edge(y, x, c);
	}
	if(!check(mod)){                    //所有城市均可选择时也无法抵达
		cout << "AFK\n"; return ;
	}
	int l = f[1], r = 1e9 + 1, mid;     //二分起点应该是第一座城市的费用,终点应当大于最大城市的费用
	while(l <= r){                      //二分答案
		mid = l + r >> 1;
		if(check(mid)) r = mid - 1;
		else l = mid + 1;
	}
	cout << l << '\n';
	return ;
}

signed main(){
	ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
	int _ = 1;
	// cin >> _;
	while(_ --){
		solve();
	}
	return 0;
}

最小值最大化


实数二分

题意
有 f 个朋友来参加派对,而我有 n 个口味大小不一的派。每位朋友都会分到一个同样大小的派,且要求每个人的派可以是一整个派,但不能是几块拼成的派。我也要给自己留一块一样的。
每个派都是高为 1,半径不等的派。
问最后每个人能分到的最大的派是多少?

思路
实数二分答案

代码

//>>>Qiansui
#include<map>
#include<set>
#include<list>
#include<stack>
#include<cmath>
#include<queue>
#include<deque>
#include<cstdio>
#include<string>
#include<vector>
#include<utility>
#include<iomanip>
#include<cstring>
#include<cstdlib>
#include<iostream>
#include<algorithm>
#include<functional>
#define ll long long
#define ull unsigned long long
#define mem(x, y) memset(x, y, sizeof(x))
#define debug(x) cout << #x << " = " << x << endl
#define debug2(x, y) cout << #x << " = " << x << " " << #y << " = "<< y << endl
//#define int long long

using namespace std;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
typedef pair<ull, ull> pull;
typedef pair<double, double> pdd;
/*
别忘了主人也是要分一块蛋糕的
*/
const int maxm = 1e4 + 5, inf = 0x3f3f3f3f, mod = 998244353;
const double pi = acos(-1.0), eps = 1e-5;
int n, f;
double r[maxm];

bool check(double mid){
	int sum = 0;
	for(int i = 1; i <= n; ++ i){
		sum += r[i] / mid;
	}
	if(sum >= f) return true;
	else return false;
}

void solve(){
	cin >> n >> f;
	++ f;
	double x = 0, y = 0, mid;
	for(int i = 1; i <= n; ++ i){
		cin >> r[i];
		r[i] = r[i] * r[i] * pi;
		y = max(y, r[i]);
	}
	while(y - x > eps){
		mid = (x + y) / 2.0;
		if(check(mid)) x = mid;
		else y = mid;
	}
	cout << fixed << setprecision(4) << y << '\n';
	return ;
}

signed main(){
	ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
	int _ = 1;
	cin >> _;
	while(_ --){
		solve();
	}
	return 0;
}

例题

简单题 abc 312 C - Invisible Hand

利用二分在范围 [1, 1e9] 内找到我们想要的满足题意的 X,而 l 和 r 转向的条件就是人数是否满足,满足则r--,找最小的 X,最终答案即为 l
代码:qiansui_code


cf 890 div2 C. To Become Max

题意
给你一个长度为 n 的序列 a,你可以执行操作:选择一个下标 i 且 i 满足 \(1 \le i \le n - 1 \; and \; a_i \le a_{i + 1}\),使得\(a[i] ++\)
问你通过最多通过 k 次操作可以获得的数组元素的最大值?

思路
枚举每一个位置上最多能执行几次操作后获得的元素最大值,利用二分枚举判断
注意到题目的构造一定是从后往前搭楼梯一般一层一层建起的,故可以利用这一特点用于判断
详见代码

代码

//>>>Qiansui
#include<bits/stdc++.h>
#define ll long long
#define ull unsigned long long
#define mem(x,y) memset(x, y, sizeof(x))
#define debug(x) cout << #x << " = " << x << '\n'
#define debug2(x,y) cout << #x << " = " << x << " " << #y << " = "<< y << '\n'
//#define int long long

using namespace std;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
typedef pair<ull, ull> pull;
typedef pair<double, double> pdd;
/*

*/
const int maxm = 1e3 + 5, inf = 0x3f3f3f3f, mod = 998244353;
ll n, k, a[maxm], ans;

ll check(int p, int h){
	ll sum = 0;
	for(int i = p, x = h; i <= n && x > a[i]; ++ i, -- x){  //判断需要的最少的花费
		if(i == n) return k + 1;        //最后一位无法操作
		sum += x - a[i];
	}
	return sum;
}

void solve(){
	cin >> n >> k;
	ans = 0;
	for(int i = 1; i <= n; ++ i){
		cin >> a[i];
		ans = max(ans, a[i]);
	}
	for(int i = 1; i <= n; ++ i){
		ll l = a[i], r = a[i] + k, mid;
		while(l <= r){
			mid = l + r >> 1;
			if(check(i, mid) <= k) l = mid + 1;
			else r = mid - 1;
		}
		ans = max(ans, r);
	}
	cout << ans << '\n';
	return ;
}

signed main(){
	ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
	int _ = 1;
	cin >> _;
	while(_ --){
		solve();
	}
	return 0;
}

洛谷 P1419 寻找段落

题意
给定一个长度为 \(n\) 的序列 \(a\),定义 \(a_i\) 为第 \(i\) 个元素的价值。现在需要找出序列中最有价值的“段落”。段落的定义是长度在 \([S, T]\) 之间的连续序列。最有价值段落是指平均值最大的段落。段落的平均值 等于 段落总价值 除以 段落长度。

思路
朴素的枚举区间显然TLE
我们重新回顾一下题面,题目要求我们找这么一个东西:$ans_{max} = \frac{\displaystyle \sum_{i = l}^{r} a_i}{r - l + 1} $ 且 $s \le r - l + 1 \le t $

将其进行转化,可以得到 \(\displaystyle \sum_{i = l}^{r} a_i = ans * (r - l + 1)\)

我们可以知道的是,答案 ans 所求的是整个序列里面的最大值,其显然满足单调性,且 ans 的范围即为 \([a_{i\ min}, a_{i\ max}]\),所以我们可以将问题转化为二分答案,判断其是否满足 \(\displaystyle \sum_{i = l}^{r} a_i \ge ans * (r - l + 1)\),即 \(\displaystyle \sum_{i = l}^{r} (a_i - x) \ge 0\)

为了加快我们的判断,我们可以对原式求前缀和,则上一式子转化为 $sum_r - sum_{l - 1} \ge 0 $
可以利用递增的单调队列优化判断
详见代码

代码

//>>>Qiansui
#include<bits/stdc++.h>
#define ll long long
#define ull unsigned long long
#define mem(x,y) memset(x, y, sizeof(x))
#define debug(x) cout << #x << " = " << x << '\n'
#define debug2(x,y) cout << #x << " = " << x << " " << #y << " = "<< y << '\n'
//#define int long long

using namespace std;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
typedef pair<ull, ull> pull;
typedef pair<double, double> pdd;
/*

*/
const int maxm = 1e5 + 5, inf = 0x3f3f3f3f, mod = 998244353;
const double eps = 1e-5;
int n, s, t, a[maxm];

bool check(double mid){
	vector<double> b(n + 1);
	for(int i = 1; i <= n; ++ i){
		b[i] = a[i] - mid + b[i - 1];
	}
	deque<int> q;//维护一个单增的单调队列
	for(int i = s, p = 0; i <= n; ++ i, ++ p){
		while(!q.empty() && b[q.back()] > b[p]) q.pop_back();   //去尾
		q.push_back(p);
		while(q.front() < i - t) q.pop_front();                 //删头
		if(b[i] - b[q.front()] >= 0) return true;               //该区间差值最大满足条件
	}
	return false;
}

void solve(){
	cin >> n >> s >> t;
	for(int i = 1; i <= n; ++ i){
		cin >> a[i];
	}
	double l = -1e4, r = 1e4, mid;
	while(r - l > eps){
		mid = (l + r) / 2.0;
		if(check(mid)) l = mid;
		else r = mid;
	}
	cout << fixed << setprecision(3) << l << '\n';  //要求输出3位!
	return ;
}

signed main(){
	ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
	int _ = 1;
	// cin >> _;
	while(_ --){
		solve();
	}
	return 0;
}

洛谷 P1083 [NOIP2012 提高组] 借教室

题意不再复述了,本题利用二分 + 差分前缀和维护暴力可以通过
二分枚举订单号 x,差分 + 前缀和维护前 x 个订单需要的教室数,再判断每天的教室数是否满足上限即可
Qiansui_code


洛谷 P2678 [NOIP2015 提高组] 跳石头

二分跳跃距离 mid,判断时跳跃距离小于 mid 的石子均移除
注意,在判断的时候需要考虑终点这一位置的影响
特殊情况见链接
Qiansui_code


ABC 319 D - Minimum Width

简单的二分答案

posted on 2023-07-30 16:46  Qiansui  阅读(14)  评论(0编辑  收藏  举报