二分算法

二分算法

二分的基础用法就是在单调函数或者单调序列中进行查找。因此,当问题的答案具有单调性时,就可以通过二分把求解转化为判定(根据复杂度理论,判定的难度小于求解)。有单调性一定可以二分,但是能二分并不一定需要有单调性,也就是说,二分的本质并不是单调性

二分搜索的应用场景:寻找一个数、寻找左侧边界、寻找右侧边界。

寻找一个数

即想要查找一个数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+(rl)/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+(rl)/2==在某种程度上是可以有效防止溢出的。

问题2:为什么while循环的条件是<=,而不是<呢?

因为初始化时r的赋值是n-1而不是n,即r是数组中最后一个元素的下标。对于== l ≤ r l\leq r lr来说,相当于左闭右闭区间 [ l , r ] [l,r] [l,r];对于 l < r l<r l<r来说,相当于左闭右开区间 [ l , r ) [l,r) [l,r)==,因为数组下标为n就会产生越界。在寻找一个数的二分算法中,我们使用的是 l ≤ r l\leq r lr,即左闭右闭区间 [ 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(lr)的终止条件是 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,mid1]或这 [ 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=mid1,而不是 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

  • C:\Users\3Code_Love\AppData\Roaming\Typora\typora-user-images\image-20210122205048570.png


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 lr满足时循环应当一直执行。但是如果想要返回第一个大于等于target的元素的位置(即寻找左侧边界),就不需要判定 t a r g e t target target本身是否存在,因为就算它不存在,返回的也是"假设它存在,它应该再数组中的位置",如果用搜索区间分析的话,因为 r = n r=n r=n,而不是 r = n − 1 r=n-1 r=n1,因此每次循环的搜索区间都是 [ 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 n1还是 n n n呢?考虑到欲查询元素有可能比序列中的所有元素都要大,那么这个target在数组中就不存在,如果它存在,那么它应该在数组最后一个元素 a [ n − 1 ] a[n-1] a[n1]之后,也就是说它应该在下标为 n n n的位置,此时应当返回 n n n(即假设它存在,它应该在的位置)。因此,二分的上界是 n n n而不是 n − 1 n-1 n1,故二分的初始区间是 [ 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=mid1,那么由于在这个算法中,搜索区间是左闭右开,那么把 m i d mid mid去掉之后,此时左区间就被划分成了 [ l , m i d − 1 ) [l,mid-1) [l,mid1),显然,我们把 m i d − 1 \color {red}{mid-1} mid1也给漏掉了。

问题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=n1 w h i l e while while循环是 w h i l e ( l ≤ r ) while(l\leq r) while(lr),它的的终止条件是 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 r1,但是不是写成返回 r r r哦。

也就是说,返回 l − 1 l-1 l1或者 r − 1 r-1 r1都是可以滴。

至于为什么要减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 l1或者 r − 1 r-1 r1的形式,而不能写成 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=n1,所以决定了搜索区间是左闭右闭区间 [ l , r ] [l,r] [l,r],也决定了 w h i l e ( l ≤ r ) while(l\leq r) while(lr),同时也决定了 l = m i d + 1 l=mid+1 l=mid+1 r = m i d − 1 r=mid-1 r=mid1。因此,只要我们一找到满足条件的一个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 l1或者 r − 1 r-1 r1


对于寻找一个数,只有一种写法,左闭右闭区间

对于寻找左侧边界,有两种写法,左闭右开区间左闭右闭区间,个人推荐写左闭右开区间

对于寻找右侧边界,有两种写法,左闭右开区间左闭右闭区间,个人推荐写左闭右开区间


下面给出了三个类型都采用左闭右闭区间的算法代码

//第一种类型   寻找一个数
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,mid1] [ 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的数。

  • 5
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卷心菜不卷Iris

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值