伤心桥下春波绿,曾是惊鸿照影来。——沈园二首

Note - 二分

引入#

二分是一种十分重要的思想。srds,我不得不举一个被用烂了的例子。

LYM 心里想了一个数 x1x10),ZWB 来猜,如果在 4 次内猜中了,ZWB 就和 JQ [数据删除]。假设 x=7

1 次,范围为 [1,10],ZWB 猜 1+102=5。LYM 甜蜜地说太小了。

2 次,范围为 [6,10],ZWB 猜 6+102=8。LYM 甜蜜地说太大了。

3 次,范围为 [6,7],ZWB 猜 6+72=6。LYM 甜蜜地说太小了。

4 次,范围为 [7,7],ZWB 猜 7+72=7。LYM 甜蜜地说恰到好处!于是,ZWB 和 LYM 就开心地 [数据删除] 了。

事实上,我们可以发现,每一次询问,我们都把区间缩短了一半,从而只需要 log210+1=4 次即可猜中。假如 ZWB 直接使用暴力 110,那么他最坏需要 10 次,就有可能不能和 JQ [数据删除] 了。

当然,最重要的二分的前提:有单调性

二分查找#

二分查找跟二分答案有一些差别,所以还是拿出来单独讲一下。

「例题」猜数字#

题意简述:给出一个序列 a,共有 n 个元素,且 ai<ai+1。请你输出元素 x 的位置,保证 x 一定在 a 中。

数据范围:1n104

这种题比较 simple 啊。类似于上面 ZWB & JQ 的猜数字,我们可以定义 l=1,r=n,然后每次都取 mid=l+r2,将 amidx 比较。如果 amid<x,我们就令 l=mid+1;如果 amid>x,我们就令 r=mid1;否则 amid=x,直接输出即可。

怎么分好像很简单,那么我们的边界是什么呢?如果 lr(有哪怕一个值没被我们排除掉),我们就一直猜。或者你直接 while (true) 也不是不行,毕竟一定找得到嘛。这样,我们查找的时间复杂度就由 O(n) 变为了 O(logn)

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e4 + 5;

int a[MAXN], n, x;
int main() {
	scanf("%d", &n);
	for (int i = 1; i <= n; i++)
		scanf("%d", &a[i]);
	scanf("%d", &x);
	int l = 1, r = n;
	while (true) {
		int mid = l + r >> 1;
		if (a[mid] < x) l = mid + 1;
		else if (a[mid] > x) r = mid - 1;
		else return printf("%d\n", mid), 0;
	}
	return 0;
}

那假如 x 不一定存在呢?我们就必须要写 while (l <= r),并且在循环结束后输出 1

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e4 + 5;

int a[MAXN], n, x;
int main() {
	scanf("%d", &n);
	for (int i = 1; i <= n; i++)
		scanf("%d", &a[i]);
	scanf("%d", &x);
	int l = 1, r = n;
	while (l <= r) {
		int mid = l + r >> 1;
		if (a[mid] < x) l = mid + 1;
		else if (a[mid] > x) r = mid - 1;
		else return printf("%d\n", mid), 0;
	}
	puts("-1");
	return 0;
}

「例题」二分查找#

题意简述:给出一个序列 a,共有 n 个元素,且 aiai+1。请你输出元素 xa 中第一次出现的位置。如果不存在输出 1

数据范围:1n106

这里我们不但要找 x,还要找到其第一次出现的位置。

  • amid>x 时,mid 一定不符合,令 r=mid1
  • amid=x 时,mid 是符合的,但不一定是第一次出现的位置,所以可以令 r=mid
  • amid<x 时,mid 一定不符合,令 l=mid+1

那什么时候停止呢?由于我们只查找一个元素,所以只要 l<r 就可以一直循环(l=r 了就直接跳出)。

#include <bits/stdc++.h>
using namespace std;

int a[1000005], n, x;
int main() {
	scanf("%d", &n);
	for (int i = 1; i <= n; i++)
		scanf("%d", &a[i]);
	scanf("%d", &x);
	int l = 1, r = n;
	while (l < r) {
		int mid = l + r >> 1;
		if (a[mid] > x) r = mid - 1;
		else if (a[mid] < x) l = mid + 1;
		else r = mid;
	}
	if (a[l] != x) puts("-1"); // 最后判断一下是否相等
	else printf("%d\n", l);
	return 0;
}

「例题」二分查找上界#

原题目链接:Link

因为上界和下界非常经典,所以先直接给出代码:

#include <cstdio>

int n, a[105], x;
int main() {
	scanf("%d", &n);
	for (int i = 1; i <= n; i++)
		scanf("%d", &a[i]);
	scanf("%d", &x);
	int l = 0, r = n;
	while (l < r) {
		int mid = l + r + 1 >> 1;
		if (a[mid] <= x) l = mid;
		else r = mid - 1;
	}
	printf("%d\n", l);
	return 0;
}
  • amid>x 时,显然区间为 [l,mid),所以令 r=mid1
  • amid=x 时,是可行的,但是不一定是最后一个位置,区间为 [mid,r],所以令 l=mid
  • amid<x 时,与等于的情况类似,可行但不一定是最后一个位置,所以也是 l=mid

最后,等于和小于的情况合并,就得到了上面的代码。

那这里 mid 为什么取的是 l+r+12 呢?如果我们写 mid = l + r >> 1,那么当 r=l+1 时,mid=l,如果进入 a[mid] <= x 分支,那么又令 l=mid,没有变化,从而死循环。而这里加了 1 就可以很好的避免这个问题。

而且,我们还可以发现,这里的 mid=l+r+12 是不可能等于 l 的,所以我们可以在开始时把 l 赋值为 0,如果结束的时候 l 还是等于 0,说明全部进的是 r = mid - 1 这个分支,即无解的情况,刚好又符合题目的要求(无解输出 0),所以最终直接输出 l 即可。

短短的十几行代码,真的十分精妙。

「习题」二分查找下界#

原题目链接:Link

试着模仿二分查找上界写出代码。

对了,STL 中有两个可爱的函数,lower_bound 和 upper_bound,实现了上界和下界的查找。用法见 Link

二分答案#

终于可以开始讲二分答案啦!!!感动!!!

一般来讲,如果一个问题的答案具有单调性,我们就可以二分这个答案,判断这个答案是否可行,然后进行相应的二分。

「例题」数列分段 Section II#

原题目链接:Link

题目要求每段和的最大值最小为多少。看到这种「最大的最小」「最小的最大」,一般都是二分。

先来看有没有单调性。题目相当于找到一个最大值 x,使得按这个值分出来的段数 m,而我们要找到 x 的最小值 ans。也就是说,当 x<ans 时,条件一定不成立;而 xans 时,条件一定成立,这就有了单调性

按照上面分析的,我们直接二分这个 x 即可。

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e5 + 5;

int n, a[MAXN], m;
int l, r;

bool check(int x) {
	int tot = 1, sum = 0;
	for (int i = 1; i <= n; i++)
		if (sum + a[i] > x) tot++, sum = a[i];
		else sum += a[i];
	return tot <= m;
} // 分出的段数不大于 m 就是可行的
int main() {
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
		l = max(l, a[i]);
		r += a[i]; // 注意 l 和 r 的范围
	}
	while (l < r) {
		int mid = l + r >> 1;
		if (check(mid)) r = mid; // 可行,缩小范围,因为可能刚好 mid 就是答案
		else l = mid + 1;
		// 不可行,即 tot > m,说明 mid 太小了,令 l = mid + 1
	}
	cout << l << endl;
	return 0;
}

「例题」砍树#

原题目链接:Link

显然,我们可以二分这个锯片的高度 mid,然后算出可以砍出的木材的数量,如果 m 就满足条件,l=mid;否则不满足 r=mid1。发现了吗?一般来讲,只可能有 l = mid 或者 l = mid + 1 这种写法,只可能有 r = mid 或者 r = mid - 1 这种写法;如果 mid 可能是答案就不会写加一减一,如果 mid 不可能是答案就一定会加一减一,把 mid 给排除掉。

Come back!这里我们写的是 l = midr = mid - 1,发现如果写 mid = l + r >> 1 的话,如果 r=l+1 且进入 l = mid 分支就会死循环,所以我们要写 mid = l + r + 1 >> 1。代码如下:

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e6 + 5;

int a[MAXN], n, m;
int l, r;

bool check(int x) {
	long long sum = 0; // 十年 OI 一场空,不开 long long 70pts
	for (int i = 1; i <= n; i++)
		if (a[i] > x) sum += a[i] - x;
	return sum >= m;
}
int main() {
	scanf("%d %d", &n, &m);
	for (int i = 1; i <= n; i++) {
		scanf("%d", &a[i]);
		r = max(r, a[i]); // 注意划分边界
		// 此处 l 无明显边界,而 r 显然不能超过 a[i] 的最大值(不然你切个屁)
	}
	while (l < r) {
		int mid = l + r + 1 >> 1;
		if (check(mid)) l = mid;
		else r = mid - 1;
	}
	printf("%d\n", l);
	return 0;
}

「例题」一元三次方程求解#

原题目链接:Link

思路一:暴力。由于根的范围在 100100,所以我们可以直接暴力枚举,复杂度 O(1)(大雾)。

思路二:实数二分。

题目中有一句关键的话:根与根之差的绝对值 1

也就是说,在 xx+1 的范围内,最多只有一个满足条件的根(x 为整数)。我们就可以枚举这个 x,然后 l=x,r=x+1 进行二分。

upd:其实隐含了一点高中数学,有个零点存在性定理,这里如果 f(mid)f(mid+1)<0 则说明在 [mid,mid+1] 有解。

由于这里不是整数,所以我们要进行实数二分。其实,实数二分比整数二分更简单,因为没有各种奇怪的加一减一,但是要限定精度。具体地说,实数二分模版如下:

while (r - l > eps) {
    double mid = (l + r) / 2;
    if (check(mid) l = mid; // 或者 r = mid
    else r = mid; // 或者 l = mid
}

其中,eps 是限定的精度。如果题目要求输出 k 位小数,我们一般可以取 eps=10(k+2)。那我们来看看这题的代码:

#include <bits/stdc++.h>
using namespace std;
const double eps = 1e-4;

double a, b, c, d;

double f(double x) {
	return a * x * x * x + b * x * x + c * x + d;
}
int main() {
	scanf("%lf %lf %lf %lf", &a, &b, &c, &d);
	for (double i = -100; i < 100; i++) {
		if (f(i) == 0) {
			printf("%.2lf ", i);
			continue;
		} // 如果自身就是根,直接 continue
		// 如果你不放心实数直接比相等,也可以写 fabs(f(i)) < 1e-16
		// 是的必须要到 1e-16,否则就 WA
		if (f(i) * f(i + 1) < 0) { // 如果之间有根,进行二分
			double l = i, r = i + 1, mid;
			while (r - l > eps) {
				mid = (l + r) / 2;
				if (f(mid) * f(l) < 0) r = mid; // 根在 [l, mid] 上
				else l = mid; // 根在 [mid, r] 上
			}
			printf("%.2lf ", l);
		}
	}
	return 0;
}

「例题」kotori的设备#

原题目链接:Link

我们先来骗下分。

什么时候我们可以无限使用这些设备呢?自然就是 i=1naip (所有设备每秒消耗的电量小于等于充电器每秒可以充的电量)的时候,因为这时一定可以让每个设备都充到需要的电。

这样你应该可以骗到 15 分(大雾)。那么接下来,因为题目标签有二分,所以我们只能二分了。那么我们肯定是二分答案(kotori 在其中一个设备能量降为 0 之前最多能使用多久的时间)。

设这个答案为 x。那么第 i 个设备在 x 秒后,消耗的电量就是 x×ai。如果原始电量 bi 足够消耗(即 bix×ai),我们就不需要给第 i 个设备充电。

那如果不够消耗(即 bi<x×ai)呢?此时电量为 bix×ai(负数)。我们就只能给它充上 0(bix×ai) ,即 x×aibi 的电量。

求出当时间等于 x 时,需要给所有设备充的电量和 s,如果 sx×px×p 就是充电器最多可以充的电量),就可行;否则不可行。

代码如下:

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005;
const double eps = 1e-5; // 题目要求 < 1e-4,我们设为 1e-5
#define ll long long

int n, p, a[MAXN], b[MAXN];
ll sum; // 最多达 1e10,int 会炸

bool check(double x) {
	double tot = 0;
	for (int i = 1; i <= n; i++)
		if (a[i] * x > b[i])
			tot += a[i] * x - b[i];
	return tot <= x * p; // 二分
}
int main() {
	scanf("%d %d", &n, &p);
	for (int i = 1; i <= n; i++) {
		scanf("%d %d", &a[i], &b[i]);
		sum += a[i];
	}
	if (sum <= p) puts("-1");
	else {
		double l = 0, r = 1e10, mid;
		while (r - l > eps) {
			if (check(mid = (l + r) / 2)) l = mid; // 压行技巧
			else r = mid;
		} // 实数二分模版
		printf("%.10lf\n", l);
	}
	return 0;
}

「例题」防线#

原题目链接:Link

题目很容易理解,就是找那唯一的有奇数个防具的位置嘛。但是这跟二分有什么关系?事实上,因为只有唯一的一个点有奇数个防具,我们就可以前缀和!那个点之前的点的前缀和都是偶数,而它本身以及后面的点的前缀和都是奇数,这样就满足了二分条件!

相信大家根据之前的分析,已经熟练掌握了恶心的加一减一以及循环条件,所以我们直接上代码:

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 2e5 + 5;

int t, n, s[MAXN], e[MAXN], d[MAXN];

int getv(int x) {
	int tot = 0;
	for (int i = 1; i <= n; i++)
		if (x >= s[i])
			tot += (min(x, e[i]) - s[i]) / d[i] + 1;
	return tot;
} // 得到在 x 前的所有防具的数量
// 这里 + 1 是因为除出来的是段数,数量还要加(植树问题)
// 这里取 min 是因为防具可能到 e[i] 就没有了,自然不能算进去
int main() {
	scanf("%d", &t);
	while (t--) {
		scanf("%d", &n);
		int l = 0, r = 0;
		for (int i = 1; i <= n; i++) {
			scanf("%d %d %d", &s[i], &e[i], &d[i]);
			r = max(r, e[i]); // r 的边界
		}
		while (l < r) {
			int mid = l + r >> 1;
			if (getv(mid) & 1) r = mid;
			else l = mid + 1;
		}
		int ans = getv(l) - getv(l - 1); // 前缀和,得到 l 点的防具数量
		if (ans & 1) printf("%d %d\n", l, ans);
		else puts("There's no weakness."); // 再判个无解
	}
	return 0;
}

三分#

为什么把三分提到这里来讲,我也不知道。反正三分跟二分挺像的,你懂吧?不过,三分适用于单峰函数。

这里有一个三分理解:Link

「例题」曲线#

原题目链接:Link

首先,你得证明这个函数可以三分。

……

证毕。那么我们现在就开始三分吧!

三分每次取的是 l,r 的三等分点,即 mid1=l+rl3mid2=rrl3。在这道题中,如果 mid1>mid2,则可以发现 mid1 一定在最小值左侧;否则 mid2 就在右侧(自己用两个手指模拟一下)。代码如下:

#include <bits/stdc++.h>
using namespace std;
const double eps = 1e-9;
const int MAXN = 1e5 + 5;

int t, n;
int a[MAXN], b[MAXN], c[MAXN];

double f(double x) {
	double res;
	for (int i = 1; i <= n; i++)
		res = i == 1 ? a[i] * x * x + b[i] * x + c[i] : max(res, a[i] * x * x + b[i] * x + c[i]);
	return res;
} // 题意中的 f 函数
int main() {
	scanf("%d", &t);
	while (t--) {
		scanf("%d", &n);
		for (int i = 1; i <= n; i++)
			scanf("%d %d %d", &a[i], &b[i], &c[i]);
		double l = 0, r = 1000;
		while (r - l > eps) {
			double mid1 = l + (r - l) / 3, mid2 = r - (r - l) / 3;
			if (f(mid1) > f(mid2)) l = mid1;
			else r = mid2;
		}
		printf("%.4lf\n", f(l));
	}
	return 0;
}

By the way,三分的精度是很高的,可能动不动就要 10 的负十几次方。

「习题」灯泡#

原题目链接:Link

这是清华集训的题目,所以可能稍微需要一点数学知识。比如相似(或者说三角函数),而且精度非常恶心,我的代码 loj 上能 A,洛谷上会被卡。

#include <bits/stdc++.h>
using namespace std;
const double eps = 1e-12;

int t;
double H, h, D;

double f(double d) {
	double v = (D - d) / (H - h) * h, res = d * (H - h) / (D - d);
	return d < v ? d + h - res : (D - d) / (H - h) * h;
}
int main() {
	cin >> t;
	while (t--) {
		cin >> H >> h >> D;
		double l = 0, r = D;
		while (r - l > eps) {
			double mid1 = l + (r - l) / 3, mid2 = r - (r - l) / 3;
			if (f(mid1) > f(mid2)) r = mid2;
			else l = mid1;
		}
		printf("%.3lf\n", f(l));
	}
	return 0;
}

upd:最后输出 fl 时加上一个 eps 即可 A,这里可以取 105

posted @   liuzimingc  阅读(131)  评论(2编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示