首先二分的思想不难,问题在于整数二分的时候如果没有处理好二分的区间,会导致死循环的情况,比如下面这种二分求上界的写法
int binarySearch(int l, int r)
{
while(l < r)
{
int mid = (l + r) / 2;
if(check(mid)) l = mid;
else r = mid;
}
return l;
}
这样写到最后二分的区间是[l, l+1),且如果check(l)返回的是true,那么将会陷入死循环,因为这时候mid始终等于l。
其次二分应用的情况很多,对于每种情况,写法也会不同,所以在这里总结下对于各种情况二分的写法
二分查找也有很多情况,例如在有序数组中找某一个数(记为a)的位置,如果只有一个数,那直接返回这个数的位置就好了,但是如果存在好几个相同的该数字,二分又可以分为找第一个出现的位置(即数组中第一个大于等于a的数的位置)或者最后一个出现的位置(即第一个大于a的数的位置的前一位),这两种就是所谓的下界和上界,这两种我们放到二分答案的情况里说明
这里我们先看每种数只有一个的情况,假设a数组元素严格递增
int binarySearch(int *a, int l, int r, int key)//key是我们要找的数
{
while(l < r)
{
int mid = (l + r) / 2;
if(a[mid] < key) l = mid + 1;
else if(a[mid] == key) return mid;
else r = mid;
}
return -1;
}
这就是最简单的二分了,我们来具体看看它的计算过程
首先该写法它的 l 和 r,即我们二分的区间的左右端点,它是保证要找的数在[l, r)里的,也就是区间左端点可能是我们要找的数,而区间右端点不可能是
当区间中点mid偏小时,l = mid + 1,二分区间变成[mid + 1, r),如果该数字存在,必定还在该区间内;
如果mid偏大,r = mid,mid不可能是我们要找的数,而r端点本来就不会是答案,所以可以这么写
所以每次二分区间[l, r),区间必定会缩小,不可能死循环,最后一次判定时(如果一直没找到该数),区间变成[l, l + 1),这时候mid = l,如果mid还不等于我们要找的数,区间就会缩小到 l == r ,便退出了循环,返回-1表示没找到
虽然该写法没什么大问题,而且比较简洁,但是我们需要注意:
二分答案是信息学竞赛中一种常用的技巧,首先我们要搞明白什么情况下可以二分答案
当我们的答案具有连续性,单调性的时候,我们就可以二分答案,那什么叫答案具有连续性呢?即可能的答案区间 [l, r] 中,x假如可能是答案,那么x - 1,x + 1可能也是答案;
什么叫单调性呢?即当x越来越大,就会越来越难以/容易满足题目要求
二分答案的过程往往会涉及到求答案的上、下界
所谓上下界,即假设我们二分的潜在答案区间为[l, r], 假设可行的答案区间为[L, R],那么L就是下界,R就是上界,即答案区间的左右端点,往往分别对应着 最少/小 和 最多/大
当我们设置二分潜在的答案区间为左闭右开的时候,即 [l, r),最终得到的 l 就是答案的上界,因为这时候 l 的右侧答案都已经不符合要求,l必定是答案中最大的了
int upperBound(int l, int r)
{
while(l + 1 < r) //循环条件为区间长度>=2
{
int mid = (l + r) / 2;
if(check(mid)) l = mid; //检查mid是否符合题目要求
else r = mid;
}
return l;
}
循环结束后区间大小就是1,即 [l, l+1),最终答案就是l
但是这样的写法需要注意以下3点:
当我们设置二分潜在的答案区间为左开右闭的时候,即 (l, r],最终得到的 r 就是答案的下界,因为这时候 r 的左侧答案都已经不符合要求,r必定是答案中最小的了
int lowerBound(int l, int r)
{
while(l + 1 < r) //循环条件为区间长度>=2
{
int mid = (l + r) / 2;
if(check(mid)) r = mid; //检查mid是否符合题目要求
else l = mid;
}
return r;
}
同样的我们也需要注意以下三点:
浮点数二分基本不会有什么问题,因为不会有整数二分取整没取好导致死循环的问题
有两种写法:
以循环次数为循环终止条件
for(int i = 0; i < 60; ++i)//循环60次就已经可以达到很高的精度了
{
mid = (l + r) / 2;
//检查mid
}
以精度位循环终止条件
while(r - l < eps)//eps为题目要求的精度
{
mid = (l + r) / 2;
//检查mid
}
比较推荐第一种写法,因为第二种如果精度设置过小,加上浮点数的精度问题还是可能死循环