ACM第一讲:二分


二分

①二分概念:

二分算法,又称折半查找,即在一个单调有序的集合中查找一个解。每次分为左右两部分,判断解在哪个部分中并调整上下界,直到找到目标元素,每次二分后都将舍弃一半的查找空间。

②时间复杂度: $O(logn)$

③二分常见模型:

  • 二分查找:在一个单调有序的区间上求解分界点。
  • 二分答案:最小值最大(最大值最小)问题,这类双最值问题常常选用二分法进行求解,也就是确定答案后,配合贪心、DP等其他算法检验这个答案是否合理,将最优化问题转换为判定性问题。

注:一般题目默认在一个从小到大的区间上。

一、整数二分

1. 查找精确值

从一个有序数组中找到一个符合要求的精确值(如猜数游戏)。如查找值为$key$的元素下标,不存在返回$-1$。

区间划分为:$[l,mid-1],mid,[mid+1,r]$。

int binary_search(int l,int r,int key)
{
    /*
    l<=r的原因:
    如果最后只剩下a[pos]和a[pos+1]这两个元素,此时l=pos,r=pos+1
    计算出mid=(l+r)/2=(pos+pos+1)/2=pos
    当a[mid]<key,此时l=mid+1=pos+1,继续检测第pos+1个元素;
    反之写成l<r时,此时会直接退出循环,不再检测第pos+1个元素,导致答案错误
    */
    while(l<=r)
    {
        int mid=(l+r)/2;
        if(a[mid]==key)
            return mid;
        //[l,mid-1]-----mid-----[mid+1,r]
        //由于mid在上个if中不成立时(即mid不是最终答案),此时可把mid排除在外
        else if(a[mid]>key)
            r=mid-1;
        else l=mid+1;
    }
    return -1;    
}

2. 查找大于等于/大于key的第一个元素

这种通常题目描述为满足某种情况的最小的元素

区间划分为:$[l,mid],[mid+1,r]$

int binary_search(int l,int r,int key)
{
    while(l<r)
    {
        /*
        此处不需要+1,即不写成mid=(l+r+1)/2;
        如果最后只剩下a[pos]和a[pos+1]这两个元素,此时l=pos,r=pos+1
        计算出mid=(l+r)/2=(pos+pos+1)/2=pos
        如果a[mid]>key,那么之后l=r=pos,跳出循环
        如果a[mid]<key,那么之后l=r=pos+1,跳出循环,故不会造成死循环
        */
        int mid=(l+r)/2;
        if(a[mid]>key)
        //如果求大于等于则写成a[mid]>=key,此处也可以是check(mid)
        //if(check(mid))
            r=mid;
        else l=mid+1;
        //如果a[mid]<key,那么a[mid]以及小于a[mid]的值都需排除在外,故l=mid+1
    }
    return l;    
}
举例:

设原数组个数为$7$,元素序列为$[1,3,5,8,10,12,15]$,用该二分查找代码查找$11$

  • 第一次:$l=1,r=7,mid=(l+r)/2=4$,此时$a[mid]=8<11$,更新$l=mid+1=5$;
  • 第二次:$l=5,r=7,mid=(l+r)/2=6$,此时$a[mid]=12>11$,更新$r=mid=6$;
  • 第三次:$l=5,r=6,mid=(l+r)/2=5$,此时$a[mid]=10<11$,更新$l=mid+1=6$,此时$l=r=6$退出循环.

最终得到结果为$12$(下标为$6$)。$12$是查到数组中大于$11$(查找数)的最小值。

故该二分代码用来解决最大值最小化问题

3. 查找小于等于/小于key的最后一个元素

这种通常题目描述为满足某种情况的最大的元素

区间划分为:$[l,mid-1],[mid,r]$

int binary_search(int l,int r,int key)
{
    while(l<r)
    {
        /*
        这里mid=(l+r+1)/2;
         如果最后只剩下a[pos]和a[pos+1]这两个元素,此时l=pos,r=pos+1
        计算出mid=(l+r+1)/2=(pos+pos+1+1)/2=pos+1
        如果a[mid]<key,l=r=pos+1,跳出循环;如果a[mid]>key,l=pos+1-1=pos,跳出循环
        */
        int mid=(l+r+1)/2;
        //如果求小于等于则写成a[mid]<=key,此处也可以是check(mid)
        if(a[mid]<key)
        //如果A[mid]<key,说明比A[mid]更小的数肯定不是小于key的最大的元素
        //所以要排除mid之前的所有元素
            l=mid;
        else r=mid-1;
        //如果A[mid]>key,那么说明A[mid]以及比A[mid]还要大的数都不可能小于key
        //所以排除A[mid]及其之后的元素。
    }
    return l;    
}
举例:

设原数组个数为$7$,元素序列为$[1,3,5,8,10,12,15]$,用该二分查找代码查找$11$

  • 第一次:$l=1,r=7,mid=(l+r+1)/2=4$,此时$a[mid]=8<11$,更新$l=mid=4$;
  • 第二次:$l=4,r=7,mid=(l+r+1)/2=6$,此时$a[mid]=12>11$,更新$r=mid-1=5$;
  • 第三次:$l=4,r=5,mid=(l+r+1)/2=5$,此时$a[mid]=10<11$,更新$l=mid=5$,此时$l=r=5$退出循环.

最终得到结果为$10$(下标为$5$)。$10$是查到数组中小于$11$(查找数)的最大值。

故该二分代码用来解决最小值最大化问题

4. 总结

最后两种情况的循环跳出条件是$l < r$,为什么不是小于等于呢?因为我们的区间变换思路是不断的舍去不可能是解的区间,最后只剩下一个数就是我们的解。而第一种情况就算最后只剩一个数也有可能不是解,所以需要使用小于等于。

  • 查找精确值,循环条件是小于等于;查找满足情况的最大最小值,循环条件是小于
  • 查找满足条件的最大数,$mid=(l+r+1)/2$;查找满足条件的最小数,$mid=(l+r)/2$
  • 如果存在没有解的情况,比如从$[1,2,3,4,5]$找出大于等于$6$的第一个数,我们只需要将最后剩下的数单独进行一次判断。

二、浮点二分

类似于数学对函数实行的二分法。

const double eps=1e-6;//精度具体题目具体分析
double binary_search(double l,double r)
{
    while(l+eps<r)
    {
        double mid=(l+r)/2;
        if(check(mid))
            r=mid;
        else l=mid;
    }
    return l;    
}

三、STL二分函数

1. 以有序数组为例

从小到大的数组中,查找区间$[l,r)$某种情况的元素$k$;

查找范围:$[l,r)$。时间复杂度为:$O(logn)$。

lower_bound(a+l,a+r,k)   //二分查找第一个大于或等于k的数字
upper_bound(a+l,a+r,k)   //二分查找第一个大于k的数字

对于$lower$_$bound$来说,当元素$k$存在时返回指向该元素的地址,如果$k$大于数组中任何一个值,则返回指向$r$的地址。如果$k$小于数组中任何一个值,则返回指向$l$的地址。同理,$upper$_$bound$类似。

  • 技巧:因为返回的是数组地址,所以可以减去数组来求得在数组中的位置。
int t=lower_bound(a+l,a+r,k)-a;
  • 此时,若$t$属于$[l,r)$,则$a[t]\geqslant k$。

2. 重载二分函数

重载为在从大到小的数组中,查找区间$[l,r)$某种情况的元素$k$,或者在一个结构体数组中实行二分查找。

lower_bound(a+l,a+r,k,greater<int>() )        
//二分查找第一个小于或等于k的数字
lower_bound(a+l,a+r,k,cmp)        
//cmp 相当于重载 <
  • 当元素$k$存在时返回指向该元素的地址,如果$k$大于数组中任何一个值,则返回指向$l$的地址。如果$k$小于数组中任何一个值,则返回指向$r$的地址。

3. 二分函数运用于其他容器中

除$vector$可变长数组,$deque$双端队列,$set$集合,$multiset$外,其他容器很少用二分函数或者不能用二分函数。

lower_bound(a.begin()+l,a.begin()+r,k) - a.begin();
//vector 可变长数组,deque 双端队列,其实和数组差不多
set<int>::iterator it = a.lower_bound(k) ;
//set ,集合,只能查找全体,返回的是迭代器,multiset和set差不多

4. 与find()比较

  • $find()$一般是$O(n)$的时间复杂度,但序列可以是无序的。
  • $lower$_$bound()$一般是$O(logn)$的时间复杂度,但序列必须的有序的。
  • 二者各有优劣,当数据量较大时尽量使用$lower$_$bound()$。

四、二分答案

  • 二分答案一般求解最小值最大(最大值最小)问题,将求解答案二分,配合贪心、DP等其他算法检验这个答案是否合理,将最优化问题转换为判定性问题。
  • 一般整数问题整数二分,实数问题浮点数二分,本质是一样的。
  • 复杂度一般为$O(nlogn)$,适合解决再$10^5$范围内的数据。

文章作者: 保底不歪抽早柚
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 保底不歪抽早柚 !
评论
  目录