二分法的基本思路是对一个有序序列(递增递减都可以)查找时,测试一个中间下标处的值,若值比期待值小,则在更大的一侧进行查找(反之亦然),查找时再次二分。这比顺序访问要少很多访问量,效率很高。
设:low,hight,mid均为整型。以在一个降序arr[5]={5,4,2,1,0}中查找k=4时的下标为例,取low=0,hight=4,则mid=low+(hight-low)/2=2(若无溢出可直接相加取半),此时arr[mid]=2小于k,这时需要向值更大的一侧(左侧)查找,所以low不变,hight=mid=2,再次拆半查找,mid=low+(hight-low)/2=2=1,此时arr[mid]=4等于k。找到结果。
这就是二分查找或者叫折半查找的基本思想。因为代码比较简单,所以无需用效率较低的迭代来实现:
int mid;
while(true){
mid=(low+hight)/2;
if(arr[mid]<k){
hight=mid;
}else if(arr[mid]>k){
low=mid;
}else{
return mid;
}
}
代码看起来就是这样的。现在来考虑这样一个问题,若上述查找中,k=3将会出现什么情况:当mid=1即arr[mid]=4时,大于k,而后出现震荡无法收敛。实在是太糟糕了,所以我们要加上一个收敛的条件,以及没有找到结果时的返回:
int mid;
while(low<hight){
mid=(low+hight)/2;
if(arr[mid]<k){
hight=mid;
}else if(arr[mid]>k){
low=mid;
}else{
return mid;
}
}
return -1;
好吧,这看起来比较完美了。现在,我们总结一下:
在二分查找中,若没有找到恰好的结果(很多时候我们不知道我们要的结果的确切值),while的退出条件即收敛条件,这个条件首先应满足low<hight,否则可能陷入无限循环。
现在看另外一个典型的例子:若函数f(x)在区间[a,b]内的单调,且f(a)<0,f(b)>0,求在该区间内的f(0)的值,精确到1E-10。此时,我们不知道要求的确切值,那么只能利用while的收敛条件:
double k=1E-10;
double mid;
//k/=100;
while(hight-low<k){
mid=hight-low;
if(f(mid)<0){
low=mid;
}else{
hight=mid;
}
}
return mid;
对于这个单调递增函数,当f(mid)比0小的时候,我们增加下限,向上查找。收敛时要求mid和实际解y之间差距不超过k,即当mid-k<=y<=mid+k时退出,最糟糕的时候low或hight正好为解,此时另一方跨越k距离正好达到精度,即hight-low=k时退出,循环条件为hight-low<k。好吧,到这里还不算完成,我们的代码还有一个小问题,如果要求保留小数点后3位,那么四舍五入还隐藏着一个问题,如:当我们的mid取到小数点后4位:mid=1.2345四舍五入保留小数点后3位为1.235,那么第四位的5是精确的吗?话句话说,mid到底是1.23449还是1.23450呢?好吧,我们还需要再向后计算一位以使得小数点后第四位为准确值。即,四舍五入保留小数点后3位时,应计算到小数点后5位:即k/=100。
下面可以说一下这个烦恼了我昨天一天,今天半上午的矩形分割了,这个问题现在长得是酱紫的:
总时间限制: 1000ms 内存限制: 65536kB
描述
平面上有一个大矩形,其左下角坐标(0,0),右上角坐标(R,R)。大矩形内部包含一些小矩形,小矩形都平行于坐标轴且互不重叠。所有矩形的顶点都是整点。要求画一根平行于y轴的直线x=k(k是整数) ,使得这些小矩形落在直线左边的面积必须大于等于落在右边的面积,且两边面积之差最小。并且,要使得大矩形在直线左边的的面积尽可能大。注意:若直线穿过一个小矩形,将会把它切成两个部分,分属左右两侧。 输入
第一行是整数R,表示大矩形的右上角坐标是(R,R) (1 <= R <= 1,000,000)。
接下来的一行是整数N,表示一共有N个小矩形(0 < N <= 10000)。
再接下来有N 行。每行有4个整数,L,T, W 和 H, 表示有一个小矩形的左上角坐标是(L,T),宽度是W,高度是H (0<=L,T <= R, 0 < W,H <= R). 小矩形不会有位于大矩形之外的部分。
输出
输出整数n,表示答案应该是直线 x=n。 如果必要的话,x=R也可以是答案。
样例输入
1000
2
1 1 2 1
5 1 2 1
样例输出
5
这个描述可能理解起来有点困难。翻译一下:
一个左下角在(0,0)宽度为R的正方形范围内,有若干不重叠的小矩形(x,y,w,h均为整数,其中w,h>0),要求在该范围内画一条x=k的直线满足如下要求:
0、k为正整数。 1、在该直线左侧的小矩形的面积和大于等于在该直线右侧小矩形的面积和。若小矩形被分割,则左侧部分计入左面面积和,右侧部分计入右侧面积和。 2、满足0、1的前提下,尽量靠右侧分割。
这个问题要求用二分法进行解答。很容易想到二分所求的结果就是左右面积差,但是这存在两个问题:
1、k取整时,如何收敛。即正好分割为左右相等时,很多情况下要把一个单位距离分割为两份,亦即切分若干小矩形。
2、尽量靠右侧分割。即直线正好落在一段没有小矩形的位置,此时右面若干单位上也没有小矩形,k如何尽量大。
先来看第一个问题:这属于我们前面提到的精度问题的类似问题——low,hight逼近解到什么程度收敛。显而易见,要求k为整数时,low+1=hight即收敛,所以循环条件为low+1<hight,或者写成hight-low>1。这样,二分结束时的low就是最小k值。
然后里解决问题二:既然k落在可能解上,那么只需要计算当前k值时左右面积差s,若右移k面积依然是s,那么继续右移,直到右移一次之后,左右面积差大于s即找到最大k。
PS:low不可能是解,因为若直线分割小矩形,则左移导致左侧面积减小,不满足题意;即使在直线不分割小矩形,也不满足尽量靠右原则。
这就是我对这个题目的理解,不过我通过的代码在解决问题1时思路不同:
一定有一个位置x可以使得左右小矩形面积差为0,但x不能用整数表示。所以,我使用了double来表示。在纠正了若干错误,提交无数次之后,得了9分……咳咳…………。这就是那份代码:
#include<iostream>
#include<cmath>
#include<cstring>
using namespace std;
struct srect{
int left;
int right;
int height;
long long s;
};
double meval(srect rect[],int rc,double x){
double ls=,rs=;
int i;
for(i=;i<rc;i++){
if(rect[i].left>=x){
rs+=rect[i].s;
}else if(rect[i].right<=x){
ls+=rect[i].s;
}else{
ls+=(x-rect[i].left)*rect[i].height;
rs+=(-x+rect[i].right)*rect[i].height;
}
}
return ls-rs;
} double mbsearch(srect rect[],int rc,double low ,double hight){
double mid,d;
while(int(low)<int(hight)){
mid=(hight+low)/;
d=meval(rect,rc,mid);
if(d<){
low=mid;
}else if(d>){
hight=mid;
}else{
return mid;
}
}
return mid;
} int main(){
int i,width,rc,br,x;
double s;
cin>>br>>rc;
srect rect[br];
for(i=;i<rc;i++){
cin>>rect[i].left>>width>>width>>rect[i].height;
rect[i].right=rect[i].left+width;
rect[i].s=width*rect[i].height;
}
x=ceil(mbsearch(rect,rc,,br));
s=meval(rect,rc,x);
while(s=meval(rect,rc,x+) && x<br){
x++;
}
cout<<x;
}
乍一看是一点问题也没有,于是在煎熬一天半之后,又煎熬了几分钟。发现评价函数返回的double值竟然…………不想等。好吧,我知道你精度挺高,但达不到1就是1的程度,修改一下比较条件:
while(abs(s-meval(rect,rc,x+1))<=1e-15 && x<br){
x++;
}
用double的最大精度限制一下就10分了。当然,用double效率要比用int要低,虽然我用了一个int(low)没有取真正的精确值。而且这份代码还有很明显的需要优化的部分:low,hight的初始值应该是所有小矩形的min(left)和max(right)——搜索的范围越小迭代次数越少。