二分查找,又叫折半查找,因为二分查找每一次查找都可以缩减掉一半的查找范围。
前提条件
使用二分查找,必须满足一个条件
1、二分的对象必须是有序的
因为,只有在有序的序列中,我们才可以通过比较决定应该删除掉哪一半。
下面我们举一个例子来方便我们了解二分的过程。
设序列为:1,2,4,7,8,10,14
要查找的值:8
第一次:取中间值7,因为8比7大,所以可以断定,8在7之后。 序列:8,10,14
第二次:取中间值10,因为8比10小,所以可以断定,8在10之前。序列:8
第三次:取到中间值8,8等于8,所以找到了8;
由此可见,我们只比较了三次就找到了8,如果使用我们习惯顺序查找法,就需要5次比较才可以找到。
当然,二分并不一定在任何情况下都是最好的,只是在大部分情况下,二分的查找速度都是比顺序查找要快的多的。
二分的使用
使用二分,我们首先要找到几个关键点
1、找到二分的对象
2、找到边界收缩的条件
数组元素查找的题,我们一班都是对其下标进行二分。
例如:在序列1,2,3,4,5,6,7,8,9中查找8元素
我么可以看见,序列是有序的,满足二分的前提条件
那么二分的对象就是数组的下标
那么边界收缩的条件呢,我们可以看,我们取中间的元素,如果这个元素比我们要找的值小,说明我们要找的值在中间元素的右边,那么我们便可以得出,左边界可以收缩,如果这个中间元素比我们要找的元素大,那么有我们要找的值就在中间元素的左边,那么,右边界可以收缩。
所以,边界收缩的条件为:
目标值<中间值 ----------->右边界收缩
目标值>中间值 ------------>左边界收缩
目标值=中间值 ------------>找到目标元素
下面以求取一个数的平方根为例(只能计算大于1的,并且不超出int类型的值)
/*
二分对象:0-num的所有实数
边界收缩条件
中间值的平方小于目标值:左边界收缩
中间值的平方大于目标值:有边界收缩
中间值的平方与目标值的误差在0.0000001范围之后,则找到最终结果值。
*/
double sqrt2(int num){
//左边界
double l=0;
//右边界
double r=num;
double mid;
while(1){
mid=(l+r)/2;
//因为计算机存储小数右误差,所以我们认为,当我们的计算结果和目标结果的误差在0.0000001之内的时候,就认为找到目标值
if(abs(mid*mid-num)<0.0000001){
return mid;
}else if(mid*mid <num){ //说明中间值结果小了,所以左边界收缩
l=mid;
}else{ //说明中间值大了。所以右边界收缩
r=mid;
}
}
return -1;
}
可以看见,我使用的循环条件是永远为真的,因为我们可以确定能够找到答案,也就是说,可以执行return mid; 语句来结束循环 ,那么如果我们不确定能够找到答案呢,应如何呢,那么这时候就需要写循环条件了。
循环条件以及边界收缩
以上我们说明了二分的使用前提条件,以及二分的对象和边界收缩的条件,那么接下来,我们就应该来找找,最终的结果应该怎么得到了。因为,并不是所有的情况都是可以通过一个条件是否满足来确定最终答案的。
就我自己而言,目前我遇到了以下几种类型
1、对数组元素的查找,可以直接找到答案的
循环条件:while(left<=right)
收缩方式:
if(a[mid]<target) left=mid+1;
if(a[mid]>target) right=mid-1;
if(a[mid]==target) return mid;
通过这种方式我们就可以将所有可能数据都找一遍,如果循环结束还没找到,就说明没有此元素。
2、给定一个精度找到一个在误差范围内的解
循环条件:while(left<=right)
收缩方式:
if(data<target) left=mid;
if(data>target) right=mid;
if(abs(data-target)<0.00001) return mid;
相对于第一种而言,知识判定答案的方法不同而已。
3、找到多个解中的最大(最小)解
比如openjudge中的1.11的第三题
这种,存在多个解的情况,我们就不能在二分查找过程中,通过比较直接返回了。
设:左部总面积=leftarea
右部总面积=rightarea
我们可以发现:
如果我们取的k, 使得leftarea<rightarea,说明,k取小了,应该变大,所有左边界收缩
如果我们取的k,使得leftarea>rightarea,这时候,我们发现,我们不知道该怎么收缩了,因为,此时要使得大矩形左部面积最大,我们还应该变大,又是左边界收缩,这样的话这个二分就进行不下去了。
此时,我们可以换一个思路,题目中要我们找到最大的解,我们可以先使用二分找到最小的解,然后再使用循环递增的方式去找最大的解。
此时边界收缩方式如下:
如果我们取的k, 使得leftarea<rightarea,说明,k取小了,应该变大,所有左边界收缩,L=mid+1;
如果我们取的k,使得leftarea>rightarea,此时因为,我们要找最小的解,所以应该将K变小,去查找是否有更小的解,所以右边界收缩,但是,在我们收缩的过程中,至少要保证有一个解,所以R=mid,绝对不能R=mid-1;否则,可能导致将解收缩掉。
代码如下:
#include<iostream>
using namespace std;
struct rectangle {
long long L;
long long T;
long long W;
long long H;
//得到矩形的左部面积
long long getLeftArea(int mid) {
long long sum = 0;
//如果在矩形左侧,则左部面积为0
if (mid <= L) {
sum = 0;
}
//如果在矩形右侧,则左部面积为矩形面积
else if (mid >= L + W) {
sum = W * H;
}
else {
sum = (mid - L) * H;
}
return sum;
}
//得到矩形的右部面积
long long getRightArea(int mid) {
long long sum = 0;
//如果在矩形左侧,则右部面积为矩形面积
if (mid <= L) {
sum = W * H;
}
//如果在矩形右侧,则右部面积为0
else if (mid >= L + W) {
sum = 0;
}
else {
sum = (L+W-mid) * H;
}
return sum;
}
}a[11000];
int n;
int R;
//得到所有矩形的左部面积
long long getLeftArea(int mid) {
long long left = 0;
for (int i = 0; i < n; i++) {
left += a[i].getLeftArea(mid);
}
return left;
}
//判断所有矩形的左部面积和是否大于等于右部面积和
bool check(int mid) {
long long left = 0;
long long right = 0;
for (int i = 0; i < n; i++) {
left += a[i].getLeftArea(mid);
right += a[i].getRightArea(mid);
}
//如果左侧面积大于等于右侧面积,则条件成立
if (left >= right) {
return true;
}
return false;
}
int main() {
cin >> R;
cin >> n;
for (int i = 0; i < n; i++) {
cin >> a[i].L >> a[i].T >> a[i].W >> a[i].H;
}
int l = 0;
int r = 1000000;
int mid;
while (l<r) {
mid = (l + r) / 2;
if (check(mid)) {
r = mid;//要保证至少一个解存在
}
else {
l = mid + 1;
}
}
//此时r即为最小解
int res = r;
//循环递增寻找最大解
while (check(res + 1) && getLeftArea(res+1)==getLeftArea(r) && res + 1 <= R) {
res++;
}
cout << res;
}