二分算法
二分的基础用法就是在单调函数或者单调序列中进行查找。因此,当问题的答案具有单调性时,就可以通过二分把求解转化为判定(根据复杂度理论,判定的难度小于求解)。有单调性一定可以二分,但是能二分并不一定需要有单调性,也就是说,二分的本质并不是单调性。
二分搜索的应用场景:寻找一个数、寻找左侧边界、寻找右侧边界。
寻找一个数
即想要查找一个数target,如果找到,则返回它在数组中的下标,如果没有找到,则返回-1。
基本代码如下
int BinarySearch(int a[],int n,int target)
{
int l=0;
int r=n-1;
while(l<=r)
{
int mid=l+(r-l)/2;
if(a[mid]==target)
reutrn mid;
else if(a[mid]<target)
l=mid+1;
else if(a[mid]>target)
r=mid-1;
}
return -1;
}
问题1:为什么mid=l+(r-l)/2而不写成mid=(l+r)/2呢?
这么写其实就是为了防止溢出。其实== m i d = l + ( r − l ) / 2 mid=l+(r-l)/2 mid=l+(r−l)/2和 m i d = ( l + r ) / 2 mid=(l+r)/2 mid=(l+r)/2是等效的。只不过如果当 l l l和 r r r太大时,那么 l + r l+r l+r就看可能会产生溢出。因此,写成 m i d = l + ( r − l ) / 2 mid=l+(r-l)/2 mid=l+(r−l)/2==在某种程度上是可以有效防止溢出的。
问题2:为什么while循环的条件是<=,而不是<呢?
因为初始化时r的赋值是n-1而不是n,即r是数组中最后一个元素的下标。对于== l ≤ r l\leq r l≤r来说,相当于左闭右闭区间 [ l , r ] [l,r] [l,r];对于 l < r l<r l<r来说,相当于左闭右开区间 [ l , r ) [l,r) [l,r)==,因为数组下标为n就会产生越界。在寻找一个数的二分算法中,我们使用的是 l ≤ r l\leq r l≤r,即左闭右闭区间 [ l , r ] [l,r] [l,r]。这个区间其实就是每次要进行所搜的区间。
问题3:什么时候应该停止搜索呢?
当我们找到了目标值target时,就可以停止搜索了。
if(a[mid]==target)
return mid;
问题4:什么时候while循环应该终止呢?
如果我们没有找到目标值target,就需要while循环终止了,然后返回-1。什么时候回需要while循环终止呢?当搜索区间为空时就应该终止。因为如果我们要搜索的区间为空时,意味着这个区间内已经没有数了,没得找了, 那就是没有找到target嘛。
w h i l e ( l ≤ r ) while(l\leq r) while(l≤r)的终止条件是 l = = r + 1 l==r+1 l==r+1,写成区间就是 [ r + 1 , r ] [r+1,r] [r+1,r]。可以带个具体的数字进去,比如 [ 5 , 4 ] [5,4] [5,4],这个时候搜索区间为空,因为既没有数字大于等于5,又没有数字小于等于4。所以这时候while循环终止就是正确的,直接返回-1就可以了。
w h i l e ( l < r ) while(l<r) while(l<r)的终止条件是 l = = r l==r l==r,写成区间就是 [ r , r ] [r,r] [r,r]。可以带个具体的数字进去,比如 [ 3 , 3 ] [3,3] [3,3],这个时候搜索区间非空,因为还有一个数字2,但是当 l = = r l==r l==r时while循环就终止了。
那么2就没有被检测到,漏掉了数组下标为2的元素没有被检查就退出了while循环,如果这时候直接返回-1就是错误的。
如果非要写成 w h i l e ( l < r ) while(l<r) while(l<r)也是可以的,只要在退出while循环后打个补丁就好了。
while(l<r)
{
//......
}
return a[l]==target?l:-1; //或者也可以写成 return a[r]==target?r:-1
问题4:为什么是l=mid+1或r=mid-1呢?有些代码是写成l=mid或者r=mid
首先要理解好搜索区间的含义,寻找一个数的二分算法它的搜索区间是左闭右闭,即 [ l , r ] [l,r] [l,r]。那么当我们发现索引 a [ m i d ] a[mid] a[mid]并不是要查找的 t a r g e t target target,那么我们就应该去搜索 [ l , m i d − 1 ] [l,mid-1] [l,mid−1]或这 [ m i d + 1 , r ] [mid+1,r] [mid+1,r]这两段区间,因为 m i d mid mid已经被检索过了, 它并不满足条件,那么就应该把它从搜索区间中去除,也即 l = m i d + 1 l=mid+1 l=mid+1或 r = m i d − 1 r=mid-1 r=mid−1,而不是 l = m i d l=mid l=mid或 r = m i d r=mid r=mid。
寻找左侧边界
寻找左侧边界其实也就是求出序列中第一个大于等于target的元素的位置L。
比如给出序列 a [ 6 ] = ( 1 , 2 , 2 , 2 , 3 , 9 ) a[6]=(1,2,2,2,3,9) a[6]=(1,2,2,2,3,9),想要寻找target=2的左侧边界,那么就应该返回数组下标1。因为 a [ 1 ] = 2 a[1]=2 a[1]=2是最左侧的边界。
思路:当我们找到 a [ m i d ] = = t a r g e t a[mid]==target a[mid]==target是先不要着急return mid,而是让 r = m i d r=mid r=mid,缩小搜索区间,让右边向左侧靠拢,使得搜索区间大体上是在左侧,这样就可以一步步地逼近我们要寻找的target的左侧边界了。
-
如果 a [ m i d ] ≥ t a r g e t a[mid]\geq target a[mid]≥target,说明第一个大于等于target的元素的位置一定是在 m i d mid mid处或者 m i d mid mid的左侧,应该往左区间 [ l , m i d ] [l,mid] [l,mid]继续缩小范围查询,即令r=mid
-
如果 a [ m i d ] ≤ t a r g e t a[mid]\leq target a[mid]≤target,说明第一个大于等于target的元素的位置一定是在 m i d mid mid的右侧,应该往右区间 [ m i d + 1 , r ] [mid+1,r] [mid+1,r]继续缩小范围查询。即令 l = m i d + 1 l=mid+1 l=mid+1
-
int left_bound(int a[],int n,int target)
{
int l=0;
int r=n; //注意!!!
while(l<r) //注意!!!
{
int mid=l+(r-l)/2;
if(a[mid]==target)
r=mid;
else if(a[mid]<target)
l=mid+1;
else if(a[mid]>target)
r=mid; //注意!!!
}
return l;
}
问题1:为什么是while(l<r)而不是while(l<=r)呢?
其实这是由问题本身的性质决定的。在寻找一个数的二分算法中,需要当元素不存在时返回-1,这样当 l > r l>r l>r时 [ l , r ] [l,r] [l,r]就不再是闭区间,可以此作为元素不存在的判定原则,因此当 l ≤ r l\leq r l≤r满足时循环应当一直执行。但是如果想要返回第一个大于等于target的元素的位置(即寻找左侧边界),就不需要判定 t a r g e t target target本身是否存在,因为就算它不存在,返回的也是"假设它存在,它应该再数组中的位置",如果用搜索区间分析的话,因为 r = n r=n r=n,而不是 r = n − 1 r=n-1 r=n−1,因此每次循环的搜索区间都是 [ l , r ) [l,r) [l,r)左闭右开区间。 w h i l e ( l < r ) while(l<r) while(l<r)的终止条件是 l = = r l==r l==r,此时搜索区间是 [ l , l ) [l,l) [l,l)为空,所以可以正确执行。因此只需要当 l < r l<r l<r时让循环一直执行就好了。由于当 l = = r l==r l==r时,while循环停止,因此最后的返回值既可以是 l l l,也可以是 r r r。
问题2:为什么是r=n,而不是r=n-1呢?
首先要明确一点,二分的初始区间应该能覆盖所以可能返回的结果。对于而二分的下界为0是很显然的。但是二分的上界是 n − 1 n-1 n−1还是 n n n呢?考虑到欲查询元素有可能比序列中的所有元素都要大,那么这个target在数组中就不存在,如果它存在,那么它应该在数组最后一个元素 a [ n − 1 ] a[n-1] a[n−1]之后,也就是说它应该在下标为 n n n的位置,此时应当返回 n n n(即假设它存在,它应该在的位置)。因此,二分的上界是 n n n而不是 n − 1 n-1 n−1,故二分的初始区间是 [ l , r ] = [ 0 , n ] [l,r]=[0,n] [l,r]=[0,n]。
问题3:我为什么没有返回-1的操作?如果数组中不存在target这个值,该怎么办呢?
首先,先来理解一下左侧边界(即第一个大于等于target的元素的位置),体会左侧边界的含义。
对于这个数组来说,要找到第一个大于等于2的元素的位置,由图可知,应该返回 i n d e x = 1 index=1 index=1。这里 i n d e x = 1 index=1 index=1的含义可以这样解读:数组a中小于2的元素个数有1个。
再比如 a [ 4 ] = ( 2 , 3 , 5 , 7 ) a[4]=(2,3,5,7) a[4]=(2,3,5,7), t a r g e t = 1 target=1 target=1,那么算法会返回0, i n d e x = 0 index=0 index=0的含义可以这样解读:数组a中小于1的元素的个数有0个。
再比如 a [ 4 ] = ( 2 , 3 , 5 , 7 ) a[4]=(2,3,5,7) a[4]=(2,3,5,7), t a r g e t = 8 target=8 target=8,那么算法会返回4, i n d e x = 4 index=4 index=4的含义可以这样解读:数组a中小于8的元素的个数有4个。
综上可知,函数的返回值(即 l l l变量的值)取值区间是闭区间 [ 0 , n ] [0,n] [0,n]。如果真想写返回-1的操作,可以再 w h i l e while while循环后面打个补丁即可。
while(l<r)
{
//.....
}
//如果target比数组中所有的元素都还要大
if(l==n)
return -1;
return a[l]==target?l:-1;
问题4:为什么是l=mid+1,r=mid呢?而不是l=mid+1,r=mid-1呢?
这个可以用搜索区间来解释,因为在寻找左侧边界的二分算法中,我们的搜索区间是左闭右开,所以当 a [ m i d ] a[mid] a[mid]被检测之后,下一步要检测的搜索区间应该去掉 m i d mid mid分割成两个区间,即== [ l , m i d ) [l,mid) [l,mid)或 [ m i d + 1 , r ) [mid+1,r) [mid+1,r)==。注意哦,是左闭右开哦,不要误以为是 [ l , m i d ] [l,mid] [l,mid]或[mid+1,r]。从中我们也可以发现,如果我们写成 r = m i d − 1 r=mid-1 r=mid−1,那么由于在这个算法中,搜索区间是左闭右开,那么把 m i d mid mid去掉之后,此时左区间就被划分成了 [ l , m i d − 1 ) [l,mid-1) [l,mid−1),显然,我们把 m i d − 1 \color {red}{mid-1} mid−1也给漏掉了。
问题5:为什么该算法能够搜索到左侧边界呢?
其实关键在对于 a [ m i d ] = = t a r g e t a[mid]==target a[mid]==target这种情况的处理
if(a[mid]==target)
r=mid;
当我们找到target时,因为我们的目标是寻找target的左侧边界,所以不能着急返回,而是缩小搜索区间的上界r,在区间 [ l , m i d ) [l,mid) [l,mid)中继续搜索,即不断向左侧收缩,达到锁定左侧边界的目的。
问题6:为什么是返回l而不是返回r呢?
其实最后返回 l l l或者返回 r r r都是可以的,因为 w h i l e while while循环的在终止条件是 l = = r l==r l==r,也就说说退出 w h i l e while while循环时, l l l和 r r r都同时指向的我们要查找的 t a r g e t target target的左侧边界(如果 t a r g e t target target存在的话),因此返回 l l l或者返回 r r r都是可以的哦。
问题7:能不能和寻找一个数的二分算法一样,写成r=n,while(l<=r)呢?
好的,安排。其实,只要你理解了搜索区间的含义之后,修改算法是很容易滴。
因为你想让搜索区间为左闭右闭,所以 r r r应该被初始化为 r = n − 1 r=n-1 r=n−1, w h i l e while while循环是 w h i l e ( l ≤ r ) while(l\leq r) while(l≤r),它的的终止条件是 l = = r + 1 l==r+1 l==r+1。
int left_bound(int a[],int n,int target)
{
int l=0;
int r=n-1;
while(l<=r)
{
int mid=l+(r-l)/2;
//....
}
}
因为搜索区间是左闭右闭,而且是搜索左侧边界,所以 l l l和 r r r的更新代码如下:
//因为搜索区间是左闭右闭,所以此时区间是[mid+1,r]
if(a[mid]<target)
l=mid+1;
//因为搜索区间是左闭右闭,所以此时区间是[l,mid-1]
else if(a[mid]>target)
r=mid-1;
//收缩右侧边界 因为搜索区间是左闭右闭,所以此时区间是[mid+1,r]
else if(a[mid]==target)
r=mid-1;
由于 w h i l e while while循环的退出条件是 l = = r + 1 l==r+1 l==r+1,所以当 t a r g e t target target比数组a中所有的元素都还要大时,会存在以下情况使得数组下标越界:
因此,最后返回结果的代码应该检查越界情况:
if(l>=n||a[l]!=target)
return -1;
return l;
至此,完整代码如下:
int left_bound(int a[],int n,int target)
{
int l=0;
int r=n-1;
//搜索区间为[l,r]
while(l<=r)
{
int mid=l+(r-l)/2;
//因为搜索区间是左闭右闭,所以此时区间是[mid+1,r]
if(a[mid]<target)
l=mid+1;
//因为搜索区间是左闭右闭,所以此时区间是[l,mid-1]
else if(a[mid]>target)
r=mid-1;
//收缩右侧边界 因为搜索区间是左闭右闭,所以此时区间是[mid+1,r]
else if(a[mid]===target)
r=mid-1;
}
// 检查出界情况
if(l>=n||a[l]!=target)
return -1;
return l;
}
寻找右侧边界
即寻找序列中第一个大于 t a r g e t target target的元素的位置。
由图可知,此时数组中第一个大于 t a r g e t target target的元素是8,其数组下标为6,因为算法应该返回6.
代码如下:
int right_bound(int a[],int n,int target)
{
int l=0;
int r=n;
//搜索区间是左闭右开,所以是[l,r)
while(l<r)
{
int mid=l+(r-l)/2;
if(a[mid]==target)
l=mid+1; //注意!!!
//搜索区间为[mid+1,r)
else if(a[mid]<target)
l=mid+1;
//搜索区间为[l,mid)
else if(a[mid]>target)
r=mid;
}
return l-1; //注意!!!
}
问题1:为什么这个算法可以找到右侧边界呢?即找到第一个大于target的元素的位置呢?
关键在对于 a [ m i d ] = = t a r g e t a[mid]==target a[mid]==target的代码处理:
if(a[mid]==target)
l=mid+1;
当 a [ m i d ] = = t a r g e t a[mid]==target a[mid]==target时,因为我们的目标是寻找target的左侧边界,所以不能着急返回,而是增大搜索区间的下界l,使得区间不断向右收缩,最终达到锁定右侧边界的目的。
问题2:为什么最后返回的是l-1而不是返回l呢?而且既然是寻找右侧边界,为什么不是返回r-1呢?
首先, w h i l e while while循环的终止条件是 l = = r l==r l==r,也就是说,当退出 w h i l e while while循环时, l l l和 r r r指向的是同一个位置,因此,如果你非要体现右侧边界,那么你也可以写成返回 r − 1 r-1 r−1,但是不是写成返回 r r r哦。
也就是说,返回 l − 1 l-1 l−1或者 r − 1 r-1 r−1都是可以滴。
至于为什么要减1,这是右侧边界的一个 特 殊 点 \color {red}{特殊点} 特殊点。关键就在于这个判断:
if(a[mid]==target)
l=mid+1;
比如此时 l = 5 l=5 l=5, r = 8 r=8 r=8, m i d = 6 mid=6 mid=6,由图可知,此时 a [ m i d ] = 2 a[mid]=2 a[mid]=2就是右侧边界,那么我们应该返回的数组下标是6。但因为此时 a [ m i d ] = = t a r g e t a[mid]==target a[mid]==target,会执行语句 l = m i d + 1 l=mid+1 l=mid+1。所以此时 l = 7 l=7 l=7, r = 8 r=8 r=8。
如图,因为 a [ m i d ] > t a r g e t a[mid]>target a[mid]>target,所以会执行语句 r = m i d r=mid r=mid,那么此时 r = 7 r=7 r=7,也就说此时 l = r = 7 l=r=7 l=r=7, l l l和 r r r都同时指向了数组下标为7的位置。
w h i l e while while循环的终止条件是 l = = r l==r l==r,如果我们写的是返回 l l l,由图可知,那么此时返回的是 l = 7 l=7 l=7,但事实上是我们应该返回的是 l = 6 l=6 l=6,因此,必须写成 l − 1 l-1 l−1或者 r − 1 r-1 r−1的形式,而不能写成 l l l或者 r r r。
问题3:为什么没有返回-1的操作?如果a数组中不存在target这个值,该怎么办呢?
这里跟寻找左侧边界类似,因为 w h i l e while while循环的终止条件是 l = = r l==r l==r,也就是说, l l l的取值范围是 [ 0 , n ] [0,n] [0,n],所以在 w h i l e while while循环后面打个补丁就好了。
while(l<r)
{
//.....
}
if(l==0)
return -1;
return a[l-1]==target?(l-1):-1;
问题4:能不能写成r=n-1,while(l<=r)的形式呢?
但是有一点要注意,当target比所有元素都要小时,r就会被减到-1,此时就会产生数组越界的问题。因此,还需要打个补丁哦。
完整代码:
int right_bound(int a[],int n,int target)
{
int l=0;
int r=n-1;
//搜索区间是左闭右闭[l,r]
while(l<=r)
{
int mid=l+(r-l)/2;
//搜索区间是左闭右闭[mid+1,r]
if(a[mid]<target)
l=mid+1;
//搜索区间是左闭右闭[l,mid-1]
else if(a[mid]>target)
r=mid-1;
//收缩左侧边界
else if(a[mid]==target)
l=mid+1;
}
//这里改为检查r越界的情况
if(r<0||a[r]!=target)
return -1;
return r;
}
总结Mark
第一个:寻找一个数。
因为初始化 l = 0 , r = n − 1 l=0,r=n-1 l=0,r=n−1,所以决定了搜索区间是左闭右闭区间 [ l , r ] [l,r] [l,r],也决定了 w h i l e ( l ≤ r ) while(l\leq r) while(l≤r),同时也决定了 l = m i d + 1 l=mid+1 l=mid+1和 r = m i d − 1 r=mid-1 r=mid−1。因此,只要我们一找到满足条件的一个target就要立即返回了,于是当 a [ m i d ] = = t a r g e t a[mid]==target a[mid]==target就可以马上返回啦。
第二个:寻找左侧边界,即寻找序列中第一个大于等于target的元素的位置
因为初始化 l = 0 , r = n l=0,r=n l=0,r=n,所以决定了搜索区间是左闭右开区间 [ l , r ) [l,r) [l,r),也决定了 w h i l e ( l < r ) while(l<r) while(l<r),同时也决定了 l = m i d + 1 l=mid+1 l=mid+1和 r = m i d r=mid r=mid。因为我们想要返回的是target的最左侧索引,所以当 a [ m i d ] = = t a r g e t a[mid]==target a[mid]==target时不能立即返回,而是要收缩右侧边界,一步步逼近左侧,最终锁定左侧边界。
第三个:寻找右侧边界,即寻找序列中第一个大于target的元素的位置,即大于某个数target的最小值,也就是小于或等于target的最大值
因为初始化 l = 0 , r = n l=0,r=n l=0,r=n,所以决定了搜索区间是左闭右开区间 [ l , r ) [l,r) [l,r),也决定了 w h i l e ( l < r ) while(l<r) while(l<r),同时也决定了 l = m i d + 1 l=mid+1 l=mid+1和 r = m i d r=mid r=mid。因为我们想要返回的是 t a r g e t target target的最右侧索引,所以当 a [ m i d ] = = t a r g e t a[mid]==target a[mid]==target时不能立即返回,而是要收缩左侧边界,一步步逼近右侧,最终锁定右侧边界。又因为收缩左侧边界时必须 l = m i d + 1 l=mid+1 l=mid+1,所以最后必须返回 l − 1 l-1 l−1或者 r − 1 r-1 r−1。
对于寻找一个数,只有一种写法,左闭右闭区间。
对于寻找左侧边界,有两种写法,左闭右开区间和左闭右闭区间,个人推荐写左闭右开区间。
对于寻找右侧边界,有两种写法,左闭右开区间和左闭右闭区间,个人推荐写左闭右开区间。
下面给出了三个类型都采用左闭右闭区间的算法代码
//第一种类型 寻找一个数
int BinarySearch(int a[],int n,int target)
{
int l=0;
int r=n-1;
while(l<r)
{
int mid=l+(r-l)/2;
if(a[mid]==target)
return mid;
//搜素区间是左闭右闭[l,mid-1]
else if(a[mid]>target)
r=mid-1;
//搜素区间是左闭右闭[mid+1,r]
else if(a[mid]<target)
l=mid+1;
}
return -1;
}
//第二种类型:寻找左侧边界
int left_bound(int a[],int n,int target)
{
int l=0;
int r=n-1;
while(l<=r)
{
int mid=l+(r-l)/2;
if(a[mid]==target)
r=mid-1;
///搜素区间是左闭右闭[l,mid-1]
else if(a[mid]>target)
r=mid-1;
//搜素区间是左闭右闭[mid+1,r]
else if(a[mid]<target)
l=mid+1;
}
//最后检查l越界的情况
if(l>=n||a[l]!=target)
return -1;
return l;
}
//第三种类型:寻找右侧边界
int right_bound(int a[],int n,int target)
{
int l=0;
int r=n-1;
while(l<=r)
{
int mid=l+(r-l)/2;
if(a[mid]==target)
l=mid+1;
else if(a[mid]<target)
l=mid+1;
else if(a[mid]>target)
r=mid-1;
}
//最后检查r越界的情况
if(r<0||a[r]!=target)
return -1;
return r;
}
二分模板
寻找左侧边界
int left_bound(int a[],int n,int target)
{
int l=0;
int r=n-1;//注意!!!
while(l<r) //注意!!!
{
int mid=l+r>>1; //注意
if(a[mid]==target)
r=mid;
else if(a[mid]>target)
r=mid;
else if(a[mid]<target)
l=mid+1;
}
return l; //或者return r
}
int left_bound(int a[],int n,int target)
{
int l=0;
int r=n-1;//注意!!!
while(l<r) //注意!!!
{
int mid=l+r>>1; //注意
if(a[mid]>=target)
r=mid;
else if(a[mid]<target)
l=mid+1;
}
return l; //或者return r
}
当 a [ m i d ] < t a r g e t a[mid]<target a[mid]<target时,说明 t a r g e t target target一定是在 m i d mid mid的右边(不包括mid,因为 a [ m i d ] a[mid] a[mid]不满足等于 t a r g e t target target这个性质),也就是说 m i d mid mid及左边的位置都被排除了,可能出现解的位置是 m i d + 1 mid+1 mid+1及其后面的位置,于是 l = m i d + 1 l=mid+1 l=mid+1。当 a [ m i d ] ≥ t a r g e t a[mid]\geq target a[mid]≥target时,说明 m i d mid mid及其左边的位置可能含有 t a r g e t target target(注意要包括 m i d mid mid,因为 a [ m i d ] a[mid] a[mid]满足等于 t a r g e t target target的这个性质)。当查找结束时, l = = r l==r l==r, l l l和 r r r都是指向 t a r g e t target target的这个位置。这种是把区间划分为两段: [ l , m i d ] [l,mid] [l,mid]和 [ m i d + 1 , r ] [mid+1,r] [mid+1,r]。这种情况并不会发生死循环,因为当发现 a [ m i d ] a[mid] a[mid]不满足某种性质时,将 l l l更新为 m i d + 1 mid+1 mid+1。所以新的区间就是 [ l , r ] = [ m i d + 1 , r ] [l,r]=[mid+1,r] [l,r]=[mid+1,r]而不再是原来的 [ l , r ] [l,r] [l,r]。
寻找右侧边界
int right_bound(int a[],int n,int target)
{
int l=0;
int r=n-1; //注意!!!
while(l<r) //注意!!!
{
int mid=l+r+1>>1; //注意!!!
if(a[mid]==target)
l=mid;
else if(a[mid]<target)
l=mid;
else if(a[mid]>target)
r=mid-1;
}
return l; //或者return r
}
int right_bound(int a[],int n,int target)
{
int l=0;
int r=n-1; //注意!!!
while(l<r) //注意!!!
{
int mid=l+r+1>>1; //注意!!!
if(a[mid]<=target)
l=mid;
else if(a[mid]>target)
r=mid-1;
}
return l; //或者return r
}
假设序列 a = ( 0 , 0 , 0 , 0 , 1 , 1 , 1 , 1 ) a=(0,0,0,0,1,1,1,1) a=(0,0,0,0,1,1,1,1),假设要查找 t a r g e t = 2 target=2 target=2的右侧边界,如果此时 a [ m i d ] = 0 a[mid]=0 a[mid]=0,说明右侧边界一定是在== m i d mid mid的右边(包括 m i d mid mid),为什么会包括 m i d mid mid呢?因为 a [ m i d ] a[mid] a[mid]满足等于 t a r g e t target target这个性质,只要满足这个性质,就说明它有可能是右侧边界(如果序列种恰好只有一个2的话),因此更新为 l = m i d l=mid l=mid。如果此时 a [ m i d ] > t a r g e t a[mid]>target a[mid]>target,即假设此时如果 a [ m i d ] = = 1 a[mid]==1 a[mid]==1,那么右侧边界一定是在 m i d mid mid的左边(不包含mid,因为 a [ m i d ] a[mid] a[mid]已经不满足等于 t a r g e t target target的性质了)。很明显,这种是把区间划分为两段: [ l , m i d − 1 ] [l,mid-1] [l,mid−1]和 [ m i d , r ] [mid,r] [mid,r]。记住哦,都是左闭右闭区间。==
问题:为什么是mid=l+r+1>>1,而不是mid=l+r>>1呢?
由图可知,如果写成 m i d = l + r > > 1 mid=l+r>>1 mid=l+r>>1,那么在第 3 3 3次循环之后, l l l一直指向 3 3 3,r一直指向 4 4 4, m i d mid mid一直指向 3 3 3,也就是说,区间一直都是 [ l , r ] = [ 3 , 4 ] [l,r]=[3,4] [l,r]=[3,4],那么就会陷入死循环。具体导致陷入死循环的原因就是:当r和l相差1时,即r-l=1时,那么mid=l+r>>1向下取整后,mid=l,接下来如果进入l=mid,那么可行区间并没有被缩小,造成死循环;如果进入r=mid-1,此时l>r(因为l=mid,r=mid-1,mid>mid-1),那么循环就不能以l=r结束。由图分析,当 l = 3 l=3 l=3, r = 4 r=4 r=4, m i d = l + r > > 1 = 3 mid=l+r>>1=3 mid=l+r>>1=3时,因为 a [ m i d ] = = t a r g e t a[mid]==target a[mid]==target,所以会进入 l = m i d l=mid l=mid,那么此时 l l l被更新为 3 3 3,然后新的区间 [ l , r ] = [ 3 , 4 ] [l,r]=[3,4] [l,r]=[3,4]。但是这个区间仍然是之前的区间,所以可行区间并没有缩小,反而是一直在 [ l , r ] = [ 3 , 4 ] [l,r]=[3,4] [l,r]=[3,4]死循环。因此,为了解决这个死循环,可以写成 m i d = l + r + 1 > > 1 mid=l+r+1>>1 mid=l+r+1>>1。当r和l相差1时,即r-l=1时,那么mid=l+r+1>>1向下取整后,mid=r,那么区间就变为[l,r]=[r,r],此时l==r,可以正确退出while循环。需下图所示。
结束第四次while循环后, l = = r l==r l==r,于是可以正确退出 w h i l e while while循环。
实数域二分
在实数域上二分比较简单,只要确定好所需的精度 e p s eps eps,以 l + e p s < r l+eps<r l+eps<r 为循环的条件,每次根据在 m i d mid mid上的判定选择 r = m i d r=mid r=mid或者 l = m i d l=mid l=mid分支之一即可。一般需要保留 k k k位小数时,则取 e p s = 1 0 − ( k + 2 ) eps=10^{-(k+2)} eps=10−(k+2).
模板
while(l+eps<r)
{
double m=l+r>>1;
if(check(mid))
r=mid;
else
l=mid;
}
代码
#include<iostream>
using namespace std;
int main()
{
double x;
cin >>x;
double l=-100,r=100;
while(l+1e-8<r)
{
double mid=l+r>>1;
if(mid*mid*mid>=x)
r=mid;
else
l=mid;
}
printf("%.6lf",l);
return 0;
}
易混淆知识点
第一类:起始位置(左侧边界)
⟺
\iff
⟺ 寻找右区间的左端点
⟺
\iff
⟺大于等于
x
x
x的第一个数
⟺
\iff
⟺大于等于
x
x
x的最小数
⟺
\iff
⟺最大值的最小,找出判定性质:
≥
x
\geq x
≥x
if(a[mid]>=x)
r=mid;
else
l=mid+1;
第二类:终止位置(右侧边界)
⟺
\iff
⟺ 寻找左区间的右端点
⟺
\iff
⟺小于等于
x
x
x的第一个数
⟺
\iff
⟺小于等于
x
x
x的最大数
⟺
\iff
⟺最小值的最大,找出判定性质:
≤
x
\leq x
≤x
if(a[mid]<=x)
l=mid;
else
r=mid-1;
lower_bound函数其实就是第一类,它与第一类是等效的,是寻找左侧边界,找到 ≥ x \geq x ≥x的第一个数。
但是upper_bound函数都不属于这两类,upper_bound函数的用处是找到第一个大于 x x x的数。