二分基础

二分

(类似于单调函数求零点)

二分查找

  • 在一个单调有序的集合中查找元素,每次将集合分为左右两部分,判断解在哪个部分中并调整集合上下界重复直到找到目标元素

题目:

给定一串n个单调递增的数,有q次询问>=x且<=y的数有多少个
数据规模:1$ \leq n \leq10^5$ 1$\leq q \leq 50000 $

手动实现
写法一
(注意下取整,避免死循环)

//找>=x的第一个位置 
while(l < r){
	mid = (l + r) / 2;
	if(a[mid] >= x) r = mid;
	else l = mid + 1;
}

//找<=x的最后一个位置 
while (l < r) {
	mid = (l + r + 1) / 2;	//因为是下取整,所以要+1,避免出现l=l的情况 eg:3 3 3出现死循环
	if(a[mid] <= x) l = mid;
	else r = mid - 1;
}

写法二
(永远把\(mid\)丢掉,但是要用\(ans=min(ans, mid)\)记录答案);

//找>=x的第一个位置
while (l <= r) {
	mid = (l + r) / 2;
	if(a[mid] >= x) r = mid - 1;	//ans是最后一次a[mid]>=x成立的mid,所以ans=r+1也就是l
	else l = mid + 1;
}

//找<=x的最后个位置
while (l <= r) {
	mid = (l + r) / 2;
	if(a[mid] <= x) l = mid + 1;	//ans是最后一次a[mid]<=x成立的mid,所以ans=l-1
	else r = mid - 1;
}

Ending Edition

//找>=x的第一个位置
//最大值的最小值
while(l <= r) {
	mid = (l + r) / 2;
	if(a[mid] >= x){
		r = mid - 1;
		ans = min(ans, mid);
	}else{
		l = mid + 1;
	}
}
//找<=x的最后一个位置
//最小值的最大值
while(l <= r) {
	mid = (l + r) / 2;
	if(a[mid] <= x){
		l = mid + 1;
		ans = min(ans, mid);
	}else{
		r = mid - 1;
	}
}

C++STL的二分查找函数

  • binary_search 返回bool值,是否存在
  • lower_bound已排好序的序列a中利用二分搜索找出指向满足\(a_i \geq k\)\(a_i\)最小的指针
  • upper_bound已排好序的序列a中利用二分搜索找出指向满足\(a_i > k\)\(a_i\)最小的指针
    如果想要知道下标减去a即可
    Eg:lower_bound(a, a + 11, 55);

USACO music notes

手写二分

#include<bits/stdc++.h>
#define ios ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)

using namespace std;
typedef long long LL;
const int N = 5e4 + 10;
int a[N], sum[N];
int n, q;

int main() {
	ios;
	cin >> n >> q;
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
		sum[i] = sum[i - 1] + a[i];
	}

	while (q--) {
		int x;
		cin >> x;
		x++;
		int l = 1, r = n;
		while (l <= r) {
			int mid = (l + r) / 2;
			if (sum[mid] >= x) r = mid - 1;
			else l = mid + 1;
		}
		cout << l << "\n";
	}


	return 0;
}

STL

#include<bits/stdc++.h>
#define ios ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)

using namespace std;
typedef long long LL;
const int N = 5e4 + 10;
int a[N], sum[N];
int n, q;

int main() {
	ios;
	cin >> n >> q;
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
		sum[i] = sum[i - 1] + a[i];
	}

	while (q--) {
		int x;
		cin >> x;
		x++;
		int ans = lower_bound(sum + 1, sum + n + 1, x) - sum;
		cout << ans << "\n";
	}
    
	return 0;
}

二分答案+检验

二分枚举+检验: 将求解类问题转化为验证类问题

牛棚

题目描述:
有N个牛棚在x轴上,已知他们的坐标。有C只奶牛,每只都必须安排在一个牛棚里,一个牛棚只能容纳一只,但是他们会互相攻击,所以要求距离最近的两个牛棚间的距离最大
(2 <= N <= 100000, 0 <= \(x_i\) <= 1000000000, 2 <= C <= N)

分析:

  • 我们可以假设距离最近的两个牛棚间的距离为x,判断这个x是否可行
  • 先把\(x_i\)排序
  • 对于一个解m,我们验证m是否可行
  • 第一个点放置牛,从左向右\(O(n)\)扫描一遍牛棚,第\(i\)个点如果和上一个放置点的距离\(\geq\)m,则在第\(i\)个点放置一头牛,统计总放置的牛数量是否\(\geq\)C
#include<bits/stdc++.h>
#define ios ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)

using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
LL a[N];
int n, c;

bool check(int x) {
	int la = a[1];
	int cnt = 1;
	for (int i = 2; i <= n; i++) {
		if (a[i] - la >= x) {
			la = a[i];
			cnt++;
		}
		if (cnt >= c) return true;
	}
	return false;
}

int main() {
	ios;
	while (scanf("%d%d", &n, &c) != EOF) {
		for (int i = 1; i <= n; i++) {
			scanf("%lld", & a[i]);
		}
		sort(a + 1, a + n + 1);

		int mid, l = 1, r = a[n];
		while (l <= r) {
			mid = (l + r) >> 1;
			if (check(mid)) l = mid + 1;
			else r = mid - 1;
		}
		cout << r << endl;
	}

	return 0;
}

Drying(poj)

题目描述:
有n件衣服需要晾干,每件含水量\(a_i\),每件衣服每分钟自然干1单位的水,每分钟可以对其中任意一件使用吹风机,其可以减少k的水,求晾干所有衣服的最少时间

  • 1 \(\leq n \leq 10^5\)
  • 1 \(\leq a_i \leq 10^9\)
  • 1 \(\leq k \leq 10^9\)
    分析:
  • 设某次二分出的一个值是mid
  • 对于一件\(a_i \leq mid\)衣服,直接晾干即可;
  • 对于一件\(a_i > mid\)的衣服,最少的用时是用吹风机一段时间,晾干一段时间,设这两段时间分别是\(x_1\)\(x_2\),那么有\(mid=x_1+x_2\), \(a_i \leq k * x_1+x_2\),解得\(x_1\geq(a_i-mid)/(k-1)\),所以对\((a_i - mid)/(k-1)\)向上取整就是该件衣服的最少用时。
  • 我们把所有的使用吹风机时间加起来,这个总和应该\(\leq mid\)
    (太毒瘤了这题)
//poj不能用万能头
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>	
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
int a[N];
int n, k;
bool judge(int x) {
	LL sum = 0;
	for (int i = 1; i <= n; i++) {
		if (a[i] <= x) continue;
		sum += ceil((a[i] - x) * 1.0 / (k - 1));	//上取整, 毒瘤点:1.吹的同时也在自然晾干 2.k=1时除0
	}
	return sum <= x;
}
int main() {

	scanf("%d", &n);
	int l = 1, r = 0;
	for (int i = 1; i <= n; i++) {
		scanf("%d", &a[i]);
		r = max(r, a[i]);
	}
	cin >> k;
	if (k == 1) {
		printf("%d", r);
		return 0;
	}
	while (l <= r) {
		int mid = (l + r) / 2;
		if (judge(mid))r = mid - 1;
		else l = mid + 1;
	}
	printf("%d", r + 1);
	return 0;
}

Chocolate Eating(USACO)

分析:

  • 二分最小的快乐值
  • 如果今天心情达不到快乐值,就吃巧克力达到为止
  • 巧克力不够则目标值过大

4 Values whose Sum is 0(poj)

分析:
既然有四列,那么我们可以分别计算前两列和后两列的和(只需要\(2n^2\)次运算),然后对后两列的和排序,那么我们对于每一个前两列的和都可以二分找到后两列的和中与之相加为0的个数,这样复杂度就是O(\(log(n)n^2\)),可以过。

#include<bits/stdc++.h>
#define ios ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
using namespace std;
typedef long long LL;

const int N = 4010;
int n, a[6][N];
int p[N * N], q[N * N];

void pre(int p[], int x, int y) {
	int cnt = 0;
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= n; j++) {
			p[++cnt] = a[x][i] + a[y][j];
		}
	}
}

int main() {
	ios;
	cin >> n;
	for (int i = 1; i <= n; i++) {
		cin >> a[1][i] >> a[2][i] >> a[3][i] >> a[4][i];
	}

	pre(p, 1, 2);
	pre(q, 3, 4);
	n = n * n;
	sort(q + 1, q + 1 + n);

	LL cnt = 0;
	for (int i = 1; i <= n; i++) {
		cnt += upper_bound(q + 1, q + 1 + n, -p[i]) - lower_bound(q + 1, q + 1 + n, -p[i]);
	}
	cout << cnt << endl;

	return 0;
}

扑克牌

分析:
直接贪心很难构造,又因为是满足单调性的(套数\(>\)答案的都不可行,\(\leq\)答案的都可行),我们直接二分答案考虑验证。

  • 如果当前待验证的数字\(x\)(即判断当前手上的牌够不够组成x套),首先,张数\(\geq\)\(x\)的牌只需要每一套用一张就行,\(<\) \(x\)的牌差几张就需要补几张\(joker\),于是我们可以计算出究竟要补几张\(joker\)
    • 如果最后需要补的\(joker\)数比手上有的\(joker\)数多,说明\(joker\)不够,不行。
    • 如果最后需要补的\(joker\)数比\(x\)还大,说明必然有两个\(joker\)在一套牌里,也不行,其他情况都是可以的。

借教室

分析:
二分第一个需要修改的申请人是第几个,然后用差分维护出来前面的这些人申请完了之后每天剩下多少个教室,如果有负的,说明之前已经有订单无法满足了,减少右界,否则增大左界。

#include<bits/stdc++.h>
#define ios ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
using namespace std;
typedef long long LL;

const int N = 1e6 + 10;
int n, m;
int r[N];

struct ty {
	int d, s, j;
} a[N];

LL delta[N];
bool check (int x) {

	for (int i = 1; i <= n; i++) {
		delta[i] = r[i] - r[i - 1];
	}
	for (int i = 1; i <= x; i++) {
		delta[a[i].s] -= a[i].d;
		delta[a[i].j + 1] += a[i].d;
	}

	LL sum = 0;
	for (int i = 1; i <= n; i++) {
		sum += delta[i];
		if (sum < 0) {
			return 0;
		}
	}
	return 1;
}

int main() {
	ios;
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		cin >> r[i];
	}
	for (int i = 1; i <= m; i++) {
		cin >> a[i].d >> a[i].s >> a[i].j;
	}

	int l = 0, r = m;
	while (l <= r) {
		int mid = (l + r) / 2;
		if (check(mid)) l = mid + 1;	//l-1最后一次能借,所以不能借是l
		else r = mid - 1;
	}

	if (l > m) cout << 0;		//完成m件订单后l不是=m,而是大于m,因为l-1是最后一次能借,l-1等于m
	else cout <<  "-1" << endl << l << endl;
	return 0;
}

总结

解决最大值最小or最小值最大的常见方法

  • 贪心
  • 二分
  • 动态规划
posted @ 2023-02-02 19:39  csai_H  阅读(36)  评论(0)    收藏  举报