二分搜索(一)—— 各種二分


  由於常年二分寫成死循環,所以是時候有必要總結一下二分搜索了,這里聲明一下本人的二分風格是左閉右開也就是[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是實際的答案有關。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM