二分查找,简而言之就是在一个有序的序列中找一个元素,因为这些元素已经有序,所以每次都将要找的数跟待寻找序列的中间元素比较,如果要找的数大于中间元素,说明接下来只需要在该序列的右半边中找,所以可以不用管左半边了,这样每次都排除一半的元素直到最后找到要找的元素。我们可以用两个变量代表待寻找序列的最左端索引(l)和最右端索引(r),然后两个变量的中间元素是mid,再经过比较之后考虑将r移动到mid处成为下一次寻找的新的r值,还是将l值移动到mid+1处成为下一次寻找的l值。
代码模板:
while(l < r){
int mid = l + (r-l) / 2; //不直接用(l+r)/2是为了防止l+r的时候溢出
if(judge(mid)) l = mid+1; //judge函数是自己定义的,判断要找的数在序列的哪半边的
else r = mid;
}
因为这样找的情况之下
需要注意一个细节的关于边界的问题:假设要在100个排好序的数中找到某数(这个数的索引最后我们得知是8),最后这个while循环只会等到 l 和 r 两个数相等的时候才会停止,当循环停止的时候 ,并不知道上一个循环的lr分别是7和8,然后是l被赋值成为mid+1得到8的,还是l与r原本为8和9,然后r被赋值成为mid的,所以这个时候需要在最后再判断一下,因为循环结束的时候l和r已经相等了,所以他们指向的位置如果不是要找的数,那么l-1(或者说r-1)处的元素才是我们要找的元素
下面给出二分法的例题
POJ 1046
https://vjudge.net/problem/POJ-1064
最最简单的二分查找
题意大概是有N根木棍,然后每一根都有着自己的长度,现在要求截断这些木棍,给出K根长度相等的木棍,问最长能截成多长(不能拼接)。
那就直接用二分法做,从0到最长木棍的长度这样一直往最合适的点逼近即可,然后每次有了猜测的长度mid之后,就遍历所有木棍,然后将每一根木棍都截出尽可能多小木棍,也就是直接 / mid ,然后加到总数中啦。
但是要注意一个特殊的地方,就是除零的时候可能出错(在下方代码中也标出来了)
#include<cstdio>
#include<iostream>
#include<vector>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn = 100500;
int N,K,k,ma = 0;
double b,ans;
int c[maxn];
bool judge(int mid, int k){
int sum = 0;
if(mid == 0) return 0; //这里如果不直接返回0的话可能在下下行 /mid的时候发生除零错
for(int i=0;i<N;i++){
sum += c[i] / mid;
if(sum >= k) return 1;
}
return 0;
}
void binsearch(int l,int r,int k){
while(l<r){
int mid = (r-l)/2+l;
if(judge(mid,k)) l = mid+1;
else r = mid;
}
if(judge(l,k)) ans = double(l) / 100.0;
else ans = double(l-1) / 100.0;
}
int main() {
#ifndef ONLINE_JUDGE
//freopen("in.txt","r",stdin);
#endif
scanf("%d%d",&N,&K);
for(int i=0;i<N;i++){
scanf("%lf",&b);
c[i] = int(b * 100);
if(c[i] > ma) ma = c[i];
}
sort(c,c+N,greater<int>());
binsearch(0,c[0],K);
if(ans < 0.01) ans = 0.00;
printf("%.2f
",ans);
return 0;
}
稍难一点的题是 POJ-2456
https://vjudge.net/problem/POJ-2456
题意比较简单,就是有N个点在数轴上排成一横排,你要从中取C个点使得这C个点中任意两个点之间的最短距离最大。首先看到任意两个点之间的最短距离,那这个距离肯定从相邻i啷个点之间的距离中来选出,因为如果a,b,c三个点依次从左到右排开,也就是说ab两个点相邻而ac两个点不相邻,那么ac之间的距离肯定大于ab。
然后这个题就是我们说到的最大化最小值的问题,因为它要满足两个要求(又要是所有间距中最小,又要是所有分配方案中最大)而贪心基本上除了一些特殊的处理(比如poj2010等等)只能满足往一个条件去贪,这样就比较难做。
但是二分就可以解决这个问题,毕竟是要找一个数嘛,那么我们其实还是一个一个地去尝试,只不过二分了之后可以每次试玩排除一半的数,这样就降低了复杂度。
基本思路就是先取l为最小距离(一般就是0啦),然后再取任意两点之间距离的最大值(也就是第一个点和最后一个点之间的距离),然后我们的所有的值就在这中间找啦。每次判断中间值是否符合要求,因为我们要找的是尽量大,所以一旦符合要求,我们就删掉左半边序列,往右半边去找,反之才往左半边去找,那么怎么才能够判断是否符合要求呢?
既然这个时候中间值mid是我们猜测的一个解,那么首先它需要满足它是所有间隔中的最小值,我们就所以任意间隔都要比他大,我们从最左边的点开始,依次遍历所有点,然后每当凑到距离>= mid了之后,就表示我们可以选这个遍历了的点,然后继续往下走,如果凑到了C个点,就说明这个mid是符合要求的,(反之如果所有点都遍历完了,还没有凑到C个点,就说明该mid不符合要求,不是我们要的解),如果符合要求了,那么再到它的右边去找,(上述操作就是代码中的judge过程,也就是判断中间这个数能否满足我们的基础要求(即所有间距中的最小值),二分的过程就是找这些值中的最大)
#include<cstdio>
#include<iostream>
#include<vector>
#include<cstring>
#include<algorithm>
#include<set>
using namespace std;
const int maxn = 100150;
int N,C;
int a[maxn];
bool judge(int x){
int cur = a[0],num = 1;
for(int i=1;i<N;i++){
if(a[i] - cur >= x) {
num++;
cur = a[i];
}
if(num >= C) return true;
}
return false;
}
void solve(){
int l = 0,r = a[N-1] - a[0];
while(l < r){
int mid = l + (r-l) / 2; //不直接用(l+r)/2是为了防止l+r的时候溢出
if(judge(mid)) l = mid+1;
else r = mid;
}
if(judge(r)) cout<<r<<endl;
else cout<<l-1<<endl;
}
int main() {
#ifndef ONLINE_JUDGE
freopen("in.txt","r",stdin);
#endif
scanf("%d%d",&N,&C);
for(int i=0;i<N;i++){
scanf("%d",&a[i]);
}
sort(a,a+N);
solve();
return 0;
}