二分查找
引言
来,玩个游戏吧,在 1 1 1 到 1000 1000 1000 内,我想一个数你来猜,当你猜数时,我会告诉你大了还是小了,你需要几次才能猜中呢?
正确的答案是: 10 10 10 次必定可以猜出来
想学吗?开始吧!
二分查找的原理
二分查找通过每次将范围缩短一半的方法来获得优秀的时间复杂度 O ( log 2 n ) O(\log_2 n) O(log2n)
比如查找在 1 1 1 到 50 50 50 以内的数字,我们选择的是数字 45 45 45 ,那么该怎么搜呢
- 初始范围 [ 1 , 50 ] [1,50] [1,50] ,我们取左右区间的平均值,也就是 25 25 25, 25 25 25 小了,所以缩短左边界
- 范围变成了 [ 25 , 50 ] [25,50] [25,50] ,继续取左右区间平均值, 37.5 37.5 37.5 ,我们取整数 37 37 37, 37 37 37 小了,所以缩短左边界
- 范围变成了 [ 37 , 50 ] [37,50] [37,50] ,重复操作,缩短左边界
- 范围变成了 [ 43 , 50 ] [43,50] [43,50]
- 范围变成了 [ 43 , 46 ] [43,46] [43,46]
- 范围变成了 [ 44 , 46 ] [44,46] [44,46]
- 范围变成了 [ 44 , 46 ] [44,46] [44,46]
- 范围变成了 [ 45 , 45 ] [45,45] [45,45]
实际上,这是 [ 1 , 50 ] [1,50] [1,50] 内最长的步骤了,去除第一步和最后一步,一共是 6 6 6 步,在 [ 1 , 50 ] [1,50] [1,50] 内,查找任意一个整数最多搜索 6 6 6 次
为什么呢?
每一次搜索范围都缩小一半,能搜几次不就是等于能除以多少个 2 2 2 吗?由于 2 6 = 64 > 50 2^6 = 64 > 50 26=64>50 ,所以 6 6 6 次之内必定能搜完
二分模板
二分的板子有很多种形式,别记那些左开右闭,左闭右开,我推荐你使用我这种超级好用的模板
//求范围[i,j]内的数
int l = i - 1, r = j + 1; //都要+1以免遗漏
while(l + 1 < r) //关键一步,不要写错
{
int mid = (l + r) / 2;
if(check(mid)) l = mid;//如果mid小了,那么将增加左边界
else r = mid;
}
关于模板为什么这样写我就不多说了,来,开始干题
刷题
二分查找题目的精髓在于 check
函数,可以说,没有 check
函数绝对写不出二分,没有不写 check
的二分
来吧!上题目
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v2RxaNrC-1690019443693)(C:\Users\david\AppData\Roaming\Typora\typora-user-images\image-20230713195212263.png)]
第一题
我们用 s [ i ] s[i] s[i] 表示 a [ 1 ] + a [ 2 ] + . . . + a [ i ] a[1] + a[2] + ...+ a[i] a[1]+a[2]+...+a[i] 即 a [ i ] a[i] a[i] 的前缀和,当一个栋楼的最大房间号小于需要查找的房间号,就说明往后、往大的搜了,否则就往前,往小的搜
输出也很有意思,输出的是第 f f f 栋楼的第 k k k 个房间
f f f 栋楼和房间号都知道了,我们怎么输出第几个房间呢?我们只需要输出当前房间号减去上一栋楼的最大房间号就可以了
#include <iostream>
#include <cstdio>
#include <algorithm>
#define int long long
using namespace std;
const int MAXN = 2e5 + 10;
int n, m;
int a[MAXN], b[MAXN], s[MAXN], mx;
int find(int x) //目标门牌号
{
int l = 0, r = n + 1; //表示几栋楼
while(l + 1 < r)
{
int mid = (l + r) >> 1;
if(s[mid] < x) l = mid; //如果s[mid]最大门牌号小于x,增加左边界
else r = mid;
}
//求最大比它小的
cout << r << ' ' << x - s[r-1] << endl;
}
signed main()
{
cin >> n >> m;
for(int i=1;i<=n;i++) cin >> a[i], s[i] = s[i-1] + a[i];
while(m--)
{
int x;
cin >> x;
find(x);
}
return 0;
}
第二题
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e8XTQg3J-1690019443695)(C:\Users\david\AppData\Roaming\Typora\typora-user-images\image-20230713200755499.png)]
这题不用前缀和,就算是二分,复杂度也比较大,是过不了这题的时空限制的
这题运用前缀和求出 ∑ j = l i r i [ w j ≥ W ] , ∑ j = l i r i [ w j ≥ W ] v i \sum_{j=l_i}^{r_i} [w_j \geq W],\sum _{j=l_i}^{r_i}[w_j \geq W]v_i ∑j=liri[wj≥W],∑j=liri[wj≥W]vi
然后再把 y i y_i yi 算出来,算出来,最后记录答案是检验结果与标准值的差而不是检验结果
a#include<iostream>
#include<cstdio>
#include<cmath>
#define int long long
using namespace std;
const int MAXN = 200010;
int n, m, s, l = 0x7fffffffffffff, r;
int w[MAXN], v[MAXN], a[MAXN], b[MAXN], ans = 0x7ffffffffffffff;
int s1[MAXN], s2[MAXN], sum;
bool check(int x)
{
for(int i=1;i<=n;i++)
{
s1[i] = s1[i-1], s2[i] = s2[i-1];
if(w[i] >= x) s1[i] += v[i], ++s2[i];
}
int y = 0;
for(int i=1;i<=m;i++) y += (s1[b[i]]-s1[a[i]-1]) * (s2[b[i]]-s2[a[i]-1]);
sum = abs(y-s);
return y > s;
}
signed main()
{
cin >> n >> m >> s;
for(int i=1;i<=n;i++)
{
scanf("%d %d", &w[i], &v[i]);
l = min(l, w[i]);
r += w[i];
}
--l, ++r;
for(int i=1;i<=m;i++) cin >> a[i] >> b[i];
while(l + 1 < r)
{
int mid = (l + r) >> 1;
if(check(mid)) l = mid;
else r = mid;
ans = min(sum, ans);
}
cout << ans;
return 0;
}
while(l + 1 < r)
{
int mid = (l + r) >> 1;
if(check(mid)) l = mid;
else r = mid;
ans = min(sum, ans);
}
cout << ans;
return 0;
}