二分
今天是黄金周的第二天,我们又开始上课了QWQ
今天讲的是二分
那我们先来看一下目录ba~
- 二分是什么
- 二分的两种板子
- 例题(openjudge 07:和为给定数)
- 二分流程总结
- 实数上的二分
0.0 二分是什么
二分二分,顾名思义,就是将查找的区间分成两半,找中间的部分,然后判断查找左半边还是右半边。
很显然,二分有一个非常重要的条件:查找元素必须有序
不然就难以判断它到底往左找还是往右找
那么,二分到底优秀在哪呢?
我们知道,评价一个算法,我们可以从时间复杂度和空间复杂度来判断
二分查找的空间复杂度:
二分查找的时间复杂度:
————————————————分割线
举个栗子
假设小明去图书馆借书,(小明:每次都叫我背锅qwq)
假设小明的书包里有100本书,但他不知道书包里的书是不是他的,他现在需要从书包里找出那本不是他的书,该怎么办呢?
(小明:吼吼吼!!!!??你太坏了,我有那么笨吗??)
我们当然可以1本1本的找,但是那样实在是太慢了!!毕竟浪费时间就是谋财害命呢!
所以工作人员想了个好办法:
- 先取出50本书,过安检机,如果没有响,那说明书在另一堆中;
- 如果安检机响了,那说明书在这一堆书当中
- 那么我们继续这两步直到最后就可以找出来那本书的位置了
那么爱思考的小朋友们不难发现,二分的好处就是把困难的求解转化成了判断,从而大大缩短了算法的时间复杂度
小明欢呼:终于可以不用被这个可恶的作者恶搞了!!
—————————————————————————手动分割线
课堂截图(捂脸)+(逃跑~~~~~~~~~~~
1.0 二分板子
!剩下两个数的时候
我在二分过程中遇到一个问题,就是剩下两个数的时候我们应该如何处理最后的两个数呢
1号位 2号位
这里我们出两个例题,请大家做完以后再看以下内容;
- 查找有序序列中第一个>=x的数,如果没有返回None
- 查找有序序列中距离x最近的<=x的数,如果没有返回None
这两个虽然看起来很像,但其实是两个不同的模板;
这里我们就应该分两种情况来讨论了,这里给大家两个板子
·
!板子1——单调递增区间查找第一个>=x的值
mid=(l+r)/2;
mid就是第一位的数
- 如果查找数
x > mid
,说明x是右面的数
至少也要从mid的下一位开始查找,查找区间为[mid+1,n]
所以l = mid +1;
- 如果查找数=mid,说明x是左面的数,即x=l;
这时候我们就需要就把右边的记号移到左边来
r = mid; - 如果查找数<mid,不存在,因为不在这一段序列中
核心代码如下
while(l<r){
mid = ( l + r ) / 2;
if(x <= a[mid]) r = mid;
else l = mid + 1;
}
!板子2——单调递增区间查找第一个<=x的值
第二种情况,我们要找第一个比x小的数
我们来分析一下这个问题
(请大家原谅我拙劣的画工,毕竟没有触控!!)
我们假设一个数列L到R,mid是中间值
我们查找的数是x
- 如果x>mid
,那么mid肯定不是答案,
查找范围最少也应该是mid+1到最后
但是加上下面这种等于的情况
查找范围就应该是mid到最后,l = mid;
2、如果x<mid
那么此时的mid是不可能成为答案的,所以右边的部分他们更没戏!
所以查找范围就变成了
核心代码
while(l<r){
mid= (l + r + 1) / 2;
if (x >= a[mid]) l = mid;
else r = mid - 1;
}
!这里注意
mid= (l + r + 1) / 2;``
想想为什么呢?
——————————————————————————分割线
看到这里,大家应该去自己去刷一点水题了
比如这个 07:和为给定数
思路:
先举个例子
1 2 4 5四个数
然后需要和为6
那么我们从1开始枚举,因为和是6,所以当枚举到每个数的时候,只需要看另一个加数在不在序列中就可以了;
比如说,1;
我们只需用二分查找,看看5在不在序列中;
放ac代码
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
int n,m;
int a[100005];
int main (){
scanf("%d",&n);
for(int i =1; i<= n;i++)
scanf("%d",&a[i]);
scanf("%d",&m);
sort(a+1,a+n+1);
for(int i = 1 ; i <= n; i++){
int l = i+1,r = n,b = m-a[i];
while(l<r){
int mid = ( l + r ) / 2;
if(b <= a[mid]) r = mid;
else l = mid + 1;
}
if(b == a[l]){
printf("%d %d\n",a[i],b);
return 0;
}
}
printf("No");
return 0;
}
P.S. 打标记法(似乎要爆空间)
先开一个数组,输入的时候数组++;
这样我们就知道每个数在数列中有几个
循环过程中:每个数–;代表取走这个数,
然后判断另一个加数是否为0个,如果不是就能输出了
这种方法就可以用O(n)的时间过
但可惜这个题用这种方法似乎爆空间了
1.X 二分流程总结
- 分析问题,确定左右半段哪个是符合题意的区间,以及mid归属于哪一段
- 分析结果,选择两种板子
mid = (l + r) / 2;
r = mid, l = mid + 1;
或者
mid = ( l + r + 1) / 2;
l = mid; r = mid - 1;
- 二分终止条件是
l == r;
也是答案所在的位置
实数上的二分
- 实数上的二分其实非常简单
- 首先要考虑精度的问题,实数的相等不能直接用==来判断,
二者差在一个很小很小的范围内我们认为就相等了;
l + eps < r
为循环条件
一般需要保留k位小数时,精度eps=10^-(k-2) - 有时不容易确定精度,我们也可以用循环固定次数的二分方法
for(int i = 0; i <= 100; i++){
double mid =(l + r)/2;
if(jud(mid)) r = mid;
else l = mid;
}
- 因为实数上不需要考虑相等的情况下,区间归属的问题
所以直接划分就好啦!
是不是很nice
while(l + 1e-6 < r){
double mid = (l + r)/2.0;
if(jud( )) l = mid; //jud里面是判断函数
else r = mid;
}
几道实数二分的题
详情请见下一篇博客——二分答案
Luogu P1024 一元三次方程求解
Luogu P1577 切绳子