zoukankan      html  css  js  c++  java
  • 二分搜索(一)—— 各种二分

      由于常年二分写成死循环,所以是时候有必要总结一下二分搜索了,这里声明一下本人的二分风格是左闭右开也就是[L,R)。

      这里就不解释什么是二分搜索了,这里将会介绍4种二分搜索,和二分搜索常用来解决的最小值最大化或者最大值最小化的问题,我们都知道使用二分的最基本条件是,我们二分的序列需要有单调性,这里的序列是广义性如:1.一个排好序的数组; 2.一个区间[L,R);3.其他(暂时想不到)。所以下面介绍的时候会用v来代表我们二分的目标,用第一个大于v,第一个大于等于v,最后一个小于v,最后一个小于等于v来描述,这里可以看到我即将要介绍的4种二分搜索。

      1.第一个大于等于v

       这就是我们常说的lower_bound()了,这是系统里面自带的库函数,在数组或者一个vector容器中二分的时候,也就是不是必须手写二分的时候首推使用这个,优点代码少,稳定(建议少装逼,动不动手写二分)。这里我们来介绍lower_bound()的使用方式。

    首先是这个函数原型:

    ForwardIterator  lower_bound (ForwardIterator first, ForwardIterator last,const T& val, Compare comp)

    其中first代表左边界迭代器,last代表右边界迭代器(注意左闭右开),val代表你要搜索的值,comp代表排序规则(这个参数在你对非结构体数组二分的时候并不需要,有默认规则)

    实例:

        int a[100]={1,2,3,3,4,5,5,6,8,9,22},n;
        while(cin>>n){
            int p=lower_bound(a,a+11,n)-a;
            //如果a是vector,那么lower(a.begin(),a.end(),v)-a.begin();
            //你也可以在指定在[L,R)区间内二分lower_bound(a.begin()+L,a.begin()+R,v)-a.begin(),数组也是同理的
            cout<<p<<endl;//这里输出的是第一个大于等于n的数的下标
        }

    当对结构体数组进行二分搜索时(我们可以在这里继续输入上面代码的初始化的数据)

    #include<bits/stdc++.h>
    using namespace std;
    struct node{
        int x;
    };
    int cmp(node a,node b){
        return a.x<b.x;//注意这里不可以a.x<=b.x不然lower_bound就变成upper_bound了
    }
    int main(){
        int n;cin>>n;
        vector<node>a(n); 
        node b;
        for(int i=0;i<n;i++) cin>>a[i].x;
        while(cin>>b.x){
            int p=lower_bound(a.begin(),a.end(),b,cmp)-a.begin();
            cout<<p<<endl;//输出还是下标
        }
        return 0;
    }

    这里我们介绍的是当数组是升序的时候的情况,如果数组是降序的,我们则需要重新定义排序规则,我们这里在使用lower_bound()就是寻找第一个小于等于v的下标。

      学会了如何使用库函数,现在我们来学习一下如何手写一个lower_bound(),我们知道二分有一个左边界L和右边界R,我们定义[L,R)内的下标都小于v,我们假设L为当前区间的答案,R为当前区间的实际答案(因为R是第一个大于等于v的下标),我们每次二分的实际上是为了让L和R不断靠近,所以当L==R的时候,我们假设的答案等于实际的答案,那么就结束循环了,返回答案L。

    #include<bits/stdc++.h>
    using namespace std;
    int main(){
        int a[100]={1,2,3,3,4,5,5,6,8,9,22},v;
        while(cin>>v){
            int L=0,R=11;
            while(L<R){
                int M=(L+R)/2;
                if(a[M]>=v) R=M;
                else L=M+1;
            }
            cout<<L<<endl;
        }    
        return 0;
    }

    注:1.当a[M]>=n时,由于R是第一个大于等于v下标,那么R最大只能是m

      2.当a[M]<n时,说明[M,R)区间内的下标都是小于v的,L作为最后的答案最小只能是M+1

     2.第一个大于v

       这就是我们常说的upper_bound()了,这是系统里面自带的库函数,这里我们来介绍upper_bound()的使用方式,和lower_bound()在可以使用的时候推荐使用。

    首先函数原型:

    ForwardIterator upper_bound (ForwardIterator first, ForwardIterator last,const T& val, Compare comp);

    其中first代表左边界迭代器,last代表右边界迭代器(注意左闭右开),val代表你要搜索的值,comp代表排序规则(这个参数在你对非结构体数组二分的时候并不需要,有默认规则)

    实例:

    int a[100]={1,2,3,3,4,5,5,6,8,9,22},n;
    while(cin>>n){
        int p=upper_bound(a,a+11,n)-a;
        //如果a是vector,那么upper_bound(a.begin(),a.end(),v)-a.begin();
        //你也可以在指定在[L,R)区间内二分upper_bound(a.begin()+L,a.begin()+R,v)-a.begin(),数组也是同理的
        cout<<p<<endl;//这里输出的是第一个大于等于n的数的下标
    }

    当对结构体数组进行二分搜索时(我们可以在这里继续输入上面代码的初始化的数据)

    #include<bits/stdc++.h>
    using namespace std;
    struct node{
        int x;
    };
    int cmp(node a,node b){
        return a.x<=b.x;//注意这里不可以a.x<=b.x不然upper_bound就变成lower_bound了
    }
    int main(){
        int n;cin>>n;
        vector<node>a(n); 
        node b;
        for(int i=0;i<n;i++) cin>>a[i].x;
        while(cin>>b.x){
            int p=upper_bound(a.begin(),a.end(),b,cmp)-a.begin();
            cout<<p<<endl;//输出还是下标
        }
        return 0;
    }

    这里我们介绍的是当数组是升序的时候的情况,如果数组是降序的,我们则需要重新定义排序规则,我们这里在使用upper_bound()就是寻找第一个小于v的下标。

      学会了如何使用库函数,现在我们来学习一下如何手写一个upper_bound(),同样的,我们定义[L,R)内的下标都小于等于v,我们假设L为当前区间的答案,R为当前区间的实际答案(因为R是第一个大于v的下标),我们每次二分的实际上是为了让L和R不断靠近,所以当L==R的时候,我们假设的答案等于实际的答案,那么就结束循环了,返回答案L。

    #include<bits/stdc++.h>
    using namespace std;
    int main(){
        int a[100]={1,2,3,3,4,5,5,6,8,9,22},v;
        while(cin>>v){
            int L=0,R=11;
            while(L<R){
                int M=(L+R)/2;
                if(a[M]>v) R=M;
                else L=M+1;
            }
            cout<<L<<endl;
        }    
        return 0;
    }

    注:1.当a[M]>n时,由于R是第一个大于v下标,那么R最大只能是M

      2.当a[M]<=n时,说明[M,R)区间内的下标都是小于等于v的,L作为最后的答案最小只能是M+1

      3.最后一个小于等于v

       上面说过了,当数组为降序的,使用lower_bound就是返回第一个小于等于下标,若一开始数组是升续的时候,那么应该先reverse一下,再用lower_bound返回下标p,则在原数组中的下标为n-p-1(假设数组有n个元素)。

        这里来介绍一下如何在如果手写一个last_less_equal()。和lower_bound二分区间[L,R)左闭右开不同,last_less_equal()的二分区间为(L,R]右闭左开。

        同样的,我们定义(L,R]内的下标都大于v,我们假设R为当前区间的答案,L为当前区间的实际答案(因为L是最后一个小于等于v的下标),我们每次二分的实际上是为了让L和R不断靠近,所以当L==R的时候,我们假设的答案等于实际的答案,那么就结束循环了,返回答案L。

    #include<bits/stdc++.h>
    using namespace std;
    int main(){
        int a[100]={1,2,3,3,4,5,5,6,8,9,22},v;
        while(cin>>v){
            int L=-1,R=10;
            while(L<R){
                int M=(L+R+1)/2;
                if(a[M]<=v) L=M;
                else R=M-1;
            }
            cout<<L<<endl;
        }    
        return 0;
    }

    注:1.当a[M]<=n时,由于L是最后一个小于等于v下标,那么L最小只能是M。

      2.当a[M]>n时,说明(L,M]区间内的下标都是大于v的,R作为最后的答案最大只能是M-1。

      4.最后一个小于v

        上面说过了,当数组为降序的,使用upper_bound就是返回第一个大于下标,若一开始数组是升续的时候,那么应该先reverse一下,再用upper_bound返回下标p,则在原数组中的下标为n-p-1(假设数组有n个元素)。

        这里来介绍一下如何在如果手写一个last_less()。和upper_bound二分区间[L,R)左闭右开不同,last_less_equal()的二分区间为(L,R]右闭左开。

        同样的,我们定义(L,R]内的下标都大于等于v,我们假设R为当前区间的答案,L为当前区间的实际答案(因为L是最后一个小于v的下标),我们每次二分的实际上是为了让L和R不断靠近,所以当L==R的时候,我们假设的答案等于实际的答案,那么就结束循环了,返回答案L。

    #include<bits/stdc++.h>
    using namespace std;
    int main(){
        int a[100]={1,2,3,3,4,5,5,6,8,9,22},v;
        while(cin>>v){
            int L=-1,R=10;
            while(L<R){
                int M=(L+R+1)/2;
                if(a[M]<v) L=M;
                else R=M-1;
            }
            cout<<L<<endl;
        }    
        return 0;
    }

    注:1.当a[M]<n时,由于L是最后一个小于v下标,那么L最小只能是M。

      2.当a[M]>=n时,说明(L,M]区间内的下标都是大于等于v的,R作为最后的答案最大只能是M-1。

      我们发现lower_bound()和upper_bound()的M=(L+R)/2,而last_less()和last_less_equal()的M=(L+R+1)/2,(L+R)/2和(L+R+1)/2的区别在于前者是向下取整,后者是向上取整,这和我们定义L或者R是实际的答案有关。

  • 相关阅读:
    P2802 【回家】
    P1706 【全排列问题】
    P1936 【水晶灯火灵】
    P1319 【压缩技术】
    P2670 【扫雷游戏】
    P1097 【统计数字】
    P1820 【寻找AP数】
    P1020 【导弹拦截】
    链表反转
    队列:队列在有限线程池中的应用
  • 原文地址:https://www.cnblogs.com/xiaowuga/p/8604750.html
Copyright © 2011-2022 走看看