基础算法 - 二分查找 / 二分答案入门
2018-01-17 21:26
465 查看
没错!你没有看错!
我来写那个让我讨厌至极的二分了!
二分真的很迷,有的时候必须用二分的地方看不出要用二分,然后就一片TLE
抱怨到这里,我们来讲讲二分的原理。
二分二分,顾名思义,就是将查找的区间分成两半,找中间的部分,然后判断查找左半边还是右半边。
很显然,二分有一个非常重要的条件:查找元素必须有序
不然就难以判断它到底往左找还是往右找
同时,要注意二分查找和二分答案在原理上相同,但二分查找用于查找元素,而二分答案更像是枚举算法的优化,在做题时不要搞错用哪一种。
那么,二分到底优秀在哪呢?
我们知道,评价一个算法,我们可以从时间复杂度和空间复杂度来判断
二分查找的空间复杂度:Θ(n),和一般算法相同
二分查找的时间复杂度:Θ(log22n),从这里就可以看出二分查找优秀了很多。
比如我们有105个元素,一般的方法在最坏情况下应查找105次,如果是二分的话,只需要log2210=7次即可。
所以二分查找确实优秀了非常多,也是不管在什么级别的比赛中都是非常重要的一种算法。
接下来我们来讲讲二分查找怎么写。
做二分查找题,在题目没有明确表示的情况下,我们最好先进行预处理
即给数组排序
这里,我果断上阵了已经喂了5圣杯的STL!
那么代码就很简单了
接下来开始二分查找
首先我们要确定查找的边界,即这个数可能在第几到第几的范围内出现。
很显然,这题我们并不能直接圈定一个范围,那么我们就无脑直接将范围设成整个数组
我们以 l 表示左边界,r 表示右边界
即
显然,左右边界在查找过程中会发生改变,所以接下来有一个问题,见下文。
预处理结束,我们就可以开始查找了。
我们之前提到了,分左右之后找中间的部分,也就是最中间的那个。
查找肯定不能一次就结束啊,所以我们要用循环。
设置变量mid为当前查找的位置。
这边有两种写法:
这里的 >> 1代表二进制右移一位,即 /21,占用时间较少。
注:位运算的优先级低于加减乘除模
接下来我们要进一步判断是向左找还是向右找。
设置变量flag代表有没有找到,初始为false。
很好理解吧,当前元素比查找元素大则我们在查找元素的右边,反之亦然。
那么有同学会注意到,为什么 l 和 r 要 +1,-1呢?
假定此时 l=2,r=3,得 mid=2 ,如果此时不是查找元素的话,那么如果当前元素比查找元素小,那么我们调整 l 的值。
如果不+1的话会发生什么呢?l 再次被赋值为2,则进入无限循环,所以我们需要+1,-1
则核心代码如下:
接下来我们举个栗子。
假定数组为
我们来找4这个元素
工程开始:
l=1 , r=8 , mid=4 , a[mid]>4 , r=mid-1=3
l=1 , r=3 , mid=2 , a[mid]<4 , l=mid+1=3
l=3 , r=3 , mid=3 , a[mid]=3 , 退出循环
工程结束。
真的很短很快有木有?!
那么二分查找可以应用在哪里呢?
LIS 及 LCS 的 Θ(nlogn) 版就会用到二分查找。
接下来就是更喜闻乐见丧心病狂的二分答案
原题链接
很多老师应该都会拿这道题当入门题吧。
看到这道题,大多数人的想法应该是暴力枚举切段长度吧。
但转头一看,原木的长度——
暴力,不存在的!
于是我们便要在答案可能在的范围中二分查找我们的答案了。
对这道题来说,答案的范围就是从1到 最长的原木长度。
于是,我们设 l=1 , r=maxn
好了,范围解决了,接下来又面临一个问题了,怎样判断答案可不可行呢?
没办法了,只能暴力了。
每根原木扫一遍,算出能切多少根,再加起来和 k 比较一下即可
判断的问题解决了,怎么判断下一步搜索的范围呢?
很显然,如果切得太少,那么长度太长,r=mid-1
如果切得太多,虽然达到了要求,此时我们可以记录答案,但长度可能太短,可能有更优解,那么 l=mid+1
代码如下:
结果:
代码 C++,0.45KB
耗时/内存 0ms, 1738KB
原创 By Venus
写的不好大佬轻喷
我来写那个让我讨厌至极的二分了!
二分真的很迷,有的时候必须用二分的地方看不出要用二分,然后就一片TLE
抱怨到这里,我们来讲讲二分的原理。
二分二分,顾名思义,就是将查找的区间分成两半,找中间的部分,然后判断查找左半边还是右半边。
很显然,二分有一个非常重要的条件:查找元素必须有序
不然就难以判断它到底往左找还是往右找
同时,要注意二分查找和二分答案在原理上相同,但二分查找用于查找元素,而二分答案更像是枚举算法的优化,在做题时不要搞错用哪一种。
那么,二分到底优秀在哪呢?
我们知道,评价一个算法,我们可以从时间复杂度和空间复杂度来判断
二分查找的空间复杂度:Θ(n),和一般算法相同
二分查找的时间复杂度:Θ(log22n),从这里就可以看出二分查找优秀了很多。
比如我们有105个元素,一般的方法在最坏情况下应查找105次,如果是二分的话,只需要log2210=7次即可。
所以二分查找确实优秀了非常多,也是不管在什么级别的比赛中都是非常重要的一种算法。
接下来我们来讲讲二分查找怎么写。
EG 1 二分查找
输入一组数据(个数为n)和一个数m,试在这组数据中找出有没有m做二分查找题,在题目没有明确表示的情况下,我们最好先进行预处理
即给数组排序
这里,我果断上阵了已经喂了5圣杯的STL!
那么代码就很简单了
sort(a+1,a+n+1);
接下来开始二分查找
首先我们要确定查找的边界,即这个数可能在第几到第几的范围内出现。
很显然,这题我们并不能直接圈定一个范围,那么我们就无脑直接将范围设成整个数组
我们以 l 表示左边界,r 表示右边界
即
l=1,r=n;
显然,左右边界在查找过程中会发生改变,所以接下来有一个问题,见下文。
预处理结束,我们就可以开始查找了。
我们之前提到了,分左右之后找中间的部分,也就是最中间的那个。
查找肯定不能一次就结束啊,所以我们要用循环。
设置变量mid为当前查找的位置。
这边有两种写法:
e774 1. FOR for(l=1,r=n;l<=r;) { mid=l+r >> 1; } 这种写法用得较少,我个人一般也不这么写 2. WHILE l=1,r=n; while(l<=r) { mid=l+r >> 1; }
这里的 >> 1代表二进制右移一位,即 /21,占用时间较少。
注:位运算的优先级低于加减乘除模
接下来我们要进一步判断是向左找还是向右找。
设置变量flag代表有没有找到,初始为false。
if(a[mid]==m)//找到了就弹出循环 { flag=true; break; } if(a[mid]>m) r=mid-1; else l=mid+1;
很好理解吧,当前元素比查找元素大则我们在查找元素的右边,反之亦然。
那么有同学会注意到,为什么 l 和 r 要 +1,-1呢?
假定此时 l=2,r=3,得 mid=2 ,如果此时不是查找元素的话,那么如果当前元素比查找元素小,那么我们调整 l 的值。
如果不+1的话会发生什么呢?l 再次被赋值为2,则进入无限循环,所以我们需要+1,-1
则核心代码如下:
sort(a+1,a+n+1); bool flag=false; int l=1,r=n; while(l<=r) { int mid=l+r >> 1; if(a[mid]==m) { flag=true; break; } if(a[mid]>m) r=mid-1; else l=mid+1; }
接下来我们举个栗子。
假定数组为
1 2 4 5 7 8 9 11
我们来找4这个元素
工程开始:
l=1 , r=8 , mid=4 , a[mid]>4 , r=mid-1=3
l=1 , r=3 , mid=2 , a[mid]<4 , l=mid+1=3
l=3 , r=3 , mid=3 , a[mid]=3 , 退出循环
工程结束。
真的很短很快有木有?!
那么二分查找可以应用在哪里呢?
LIS 及 LCS 的 Θ(nlogn) 版就会用到二分查找。
接下来就是更喜闻乐见丧心病狂的二分答案
EG 2 二分答案
例题:Luogu P2440 木材加工原题链接
很多老师应该都会拿这道题当入门题吧。
看到这道题,大多数人的想法应该是暴力枚举切段长度吧。
但转头一看,原木的长度——
暴力,不存在的!
于是我们便要在答案可能在的范围中二分查找我们的答案了。
对这道题来说,答案的范围就是从1到 最长的原木长度。
于是,我们设 l=1 , r=maxn
好了,范围解决了,接下来又面临一个问题了,怎样判断答案可不可行呢?
没办法了,只能暴力了。
每根原木扫一遍,算出能切多少根,再加起来和 k 比较一下即可
判断的问题解决了,怎么判断下一步搜索的范围呢?
很显然,如果切得太少,那么长度太长,r=mid-1
如果切得太多,虽然达到了要求,此时我们可以记录答案,但长度可能太短,可能有更优解,那么 l=mid+1
代码如下:
#include<cstdio> #include<algorithm> using namespace std; int n,k,a[100005],l,r,mid,ans,sum,maxn; bool Lets_Cut(int m) { sum=0;//设置初值 for(int i=1;i<=n;i++) sum+=a[i]/m;//计算能切多少 if(sum>=k) return 1;//如果切够了返回true else return 0;//不够返回false } int main() { scanf("%d %d",&n,&k); for(int i=1;i<=n;i++) scanf("%d",&a[i]),maxn=max(maxn,a[i]); l=1; r=maxn; while(l<=r) { mid=l+r >> 1; if(Lets_Cut(mid)) { ans=max(ans,mid); l=mid+1; } else r=mid-1; }//全部同上 printf("%d",ans); return 0; }
结果:
代码 C++,0.45KB
耗时/内存 0ms, 1738KB
总结
二分是OI中非常重要的一种优化的算法,可以优化非常巨量的时间复杂度,有很大的必要深入研究与练习原创 By Venus
写的不好大佬轻喷
相关文章推荐
- java算法基础--二分查找
- ACM算法入门——二分查找
- 算法基础(一)——冒泡,简单选择排序,二分查找
- 算法入门1:二分查找
- 二分查找--基础算法
- [ACM] [算法基础] POJ 4140 方程求解(二分查找)
- 【算法基础】二分查找
- [ACM] [算法基础] POJ 2503 在线翻译Babelfish(二分查找 字符串)
- Java基础复习 查找算法之二分法
- 一步一步复习数据结构和算法基础-顺序查找+二分查找
- 二分答案:Poweroj2461-入门基础之二分答案(二分法的应用)
- 基础算法5——二分查找
- 基础算法 二分查找
- 二分算法入门——二分查找
- php 基础算法之 二分查找
- 算法入门---java语言实现的二分查找小结
- 基础算法学习(01)-二分查找
- 数据结构&算法(二)_算法基础之前传(递归、时间复杂度、空间复杂度、二分查找)
- Java基础算法:堆排,快排,二分查找
- 算法基础之python实现贪心算法中圣诞老人分糖果问题和二分查找算法中烘干衣服问题