Note - 二分
引入#
二分是一种十分重要的思想。srds,我不得不举一个被用烂了的例子。
LYM 心里想了一个数 (),ZWB 来猜,如果在 次内猜中了,ZWB 就和 JQ [数据删除]。假设 。
第 次,范围为 ,ZWB 猜 。LYM 甜蜜地说太小了。
第 次,范围为 ,ZWB 猜 。LYM 甜蜜地说太大了。
第 次,范围为 ,ZWB 猜 。LYM 甜蜜地说太小了。
第 次,范围为 ,ZWB 猜 。LYM 甜蜜地说恰到好处!于是,ZWB 和 LYM 就开心地 [数据删除] 了。
事实上,我们可以发现,每一次询问,我们都把区间缩短了一半,从而只需要 次即可猜中。假如 ZWB 直接使用暴力 ,那么他最坏需要 次,就有可能不能和 JQ [数据删除] 了。
当然,最重要的二分的前提:有单调性!
二分查找#
二分查找跟二分答案有一些差别,所以还是拿出来单独讲一下。
「例题」猜数字#
题意简述:给出一个序列 ,共有 个元素,且 。请你输出元素 的位置,保证 一定在 中。
数据范围:。
这种题比较 simple 啊。类似于上面 ZWB & JQ 的猜数字,我们可以定义 ,然后每次都取 ,将 与 比较。如果 ,我们就令 ;如果 ,我们就令 ;否则 ,直接输出即可。
怎么分好像很简单,那么我们的边界是什么呢?如果 (有哪怕一个值没被我们排除掉),我们就一直猜。或者你直接 while (true)
也不是不行,毕竟一定找得到嘛。这样,我们查找的时间复杂度就由 变为了 。
#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;
}
那假如 不一定存在呢?我们就必须要写 while (l <= r)
,并且在循环结束后输出 。
#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;
}
「例题」二分查找#
题意简述:给出一个序列 ,共有 个元素,且 。请你输出元素 在 中第一次出现的位置。如果不存在输出 。
数据范围:。
这里我们不但要找 ,还要找到其第一次出现的位置。
- 当 时, 一定不符合,令 。
- 当 时, 是符合的,但不一定是第一次出现的位置,所以可以令 。
- 当 时, 一定不符合,令 。
那什么时候停止呢?由于我们只查找一个元素,所以只要 就可以一直循环( 了就直接跳出)。
#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;
}
- 当 时,显然区间为 ,所以令 。
- 当 时,是可行的,但是不一定是最后一个位置,区间为 ,所以令 。
- 当 时,与等于的情况类似,可行但不一定是最后一个位置,所以也是 。
最后,等于和小于的情况合并,就得到了上面的代码。
那这里 为什么取的是 呢?如果我们写 mid = l + r >> 1
,那么当 时,,如果进入 a[mid] <= x
分支,那么又令 ,没有变化,从而死循环。而这里加了 就可以很好的避免这个问题。
而且,我们还可以发现,这里的 是不可能等于 的,所以我们可以在开始时把 赋值为 ,如果结束的时候 还是等于 ,说明全部进的是 r = mid - 1
这个分支,即无解的情况,刚好又符合题目的要求(无解输出 ),所以最终直接输出 即可。
短短的十几行代码,真的十分精妙。
「习题」二分查找下界#
原题目链接:Link。
试着模仿二分查找上界写出代码。
对了,STL 中有两个可爱的函数,lower_bound 和 upper_bound,实现了上界和下界的查找。用法见 Link。
二分答案#
终于可以开始讲二分答案啦!!!感动!!!
一般来讲,如果一个问题的答案具有单调性,我们就可以二分这个答案,判断这个答案是否可行,然后进行相应的二分。
「例题」数列分段 Section II#
原题目链接:Link。
题目要求每段和的最大值最小为多少。看到这种「最大的最小」「最小的最大」,一般都是二分。
先来看有没有单调性。题目相当于找到一个最大值 ,使得按这个值分出来的段数 ,而我们要找到 的最小值 。也就是说,当 时,条件一定不成立;而 时,条件一定成立,这就有了单调性!
按照上面分析的,我们直接二分这个 即可。
#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。
显然,我们可以二分这个锯片的高度 ,然后算出可以砍出的木材的数量,如果 就满足条件,;否则不满足 。发现了吗?一般来讲,只可能有 l = mid
或者 l = mid + 1
这种写法,只可能有 r = mid
或者 r = mid - 1
这种写法;如果 可能是答案就不会写加一减一,如果 不可能是答案就一定会加一减一,把 给排除掉。
Come back!这里我们写的是 l = mid
和 r = mid - 1
,发现如果写 mid = l + r >> 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。
思路一:暴力。由于根的范围在 ,所以我们可以直接暴力枚举,复杂度 (大雾)。
思路二:实数二分。
题目中有一句关键的话:根与根之差的绝对值 。
也就是说,在 的范围内,最多只有一个满足条件的根( 为整数)。我们就可以枚举这个 ,然后 进行二分。
upd:其实隐含了一点高中数学,有个零点存在性定理,这里如果 则说明在 有解。
由于这里不是整数,所以我们要进行实数二分。其实,实数二分比整数二分更简单,因为没有各种奇怪的加一减一,但是要限定精度。具体地说,实数二分模版如下:
while (r - l > eps) {
double mid = (l + r) / 2;
if (check(mid) l = mid; // 或者 r = mid
else r = mid; // 或者 l = mid
}
其中, 是限定的精度。如果题目要求输出 位小数,我们一般可以取 。那我们来看看这题的代码:
#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。
我们先来骗下分。
什么时候我们可以无限使用这些设备呢?自然就是 (所有设备每秒消耗的电量小于等于充电器每秒可以充的电量)的时候,因为这时一定可以让每个设备都充到需要的电。
这样你应该可以骗到 分(大雾)。那么接下来,因为题目标签有二分,所以我们只能二分了。那么我们肯定是二分答案(kotori 在其中一个设备能量降为 0 之前最多能使用多久的时间)。
设这个答案为 。那么第 个设备在 秒后,消耗的电量就是 。如果原始电量 足够消耗(即 ),我们就不需要给第 个设备充电。
那如果不够消耗(即 )呢?此时电量为 (负数)。我们就只能给它充上 ,即 的电量。
求出当时间等于 时,需要给所有设备充的电量和 ,如果 ( 就是充电器最多可以充的电量),就可行;否则不可行。
代码如下:
#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。
首先,你得证明这个函数可以三分。
……
证毕。那么我们现在就开始三分吧!
三分每次取的是 的三等分点,即 和 。在这道题中,如果 ,则可以发现 一定在最小值左侧;否则 就在右侧(自己用两个手指模拟一下)。代码如下:
#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,三分的精度是很高的,可能动不动就要 的负十几次方。
「习题」灯泡#
原题目链接: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:最后输出 时加上一个 即可 A,这里可以取 。
Posted by liuzimingc
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」