1. 程式人生 > >二分查詢 / 二分答案入門

二分查詢 / 二分答案入門

沒錯!你沒有看錯!
我來寫那個讓我討厭至極的二分了!
二分真的很迷,有的時候必須用二分的地方看不出要用二分,然後就一片TLE
抱怨到這裡,我們來講講二分的原理。
二分二分,顧名思義,就是將查詢的區間分成兩半,找中間的部分,然後判斷查詢左半邊還是右半邊。
很顯然,二分有一個非常重要的條件:查詢元素必須有序
不然就難以判斷它到底往左找還是往右找
同時,要注意二分查詢和二分答案在原理上相同,但二分查詢用於查詢元素,而二分答案更像是列舉演算法的優化,在做題時不要搞錯用哪一種。
那麼,二分到底優秀在哪呢?

我們知道,評價一個演算法,我們可以從時間複雜度空間複雜度來判斷
二分查詢的空間複雜度:Θ

(n),和一般演算法相同
二分查詢的時間複雜度:Θ(log22n),從這裡就可以看出二分查詢優秀了很多。
比如我們有105個元素,一般的方法在最壞情況下應查詢105次,如果是二分的話,只需要log2210=7次即可。
所以二分查詢確實優秀了非常多,也是不管在什麼級別的比賽中都是非常重要的一種演算法。
接下來我們來講講二分查詢怎麼寫。

EG 1 二分查詢

輸入一組資料(個數為n)和一個數m,試在這組資料中找出有沒有m

做二分查詢題,在題目沒有明確表示的情況下,我們最好先進行預處理
即給陣列排序
這裡,我果斷上陣了已經餵了5聖盃的STL!
那麼程式碼就很簡單了

sort(a+1,a+n+1
);

接下來開始二分查詢
首先我們要確定查詢的邊界,即這個數可能在第幾到第幾的範圍內出現。
很顯然,這題我們並不能直接圈定一個範圍,那麼我們就無腦直接將範圍設成整個陣列
我們以 l 表示左邊界,r 表示右邊界

l=1,r=n;

顯然,左右邊界在查詢過程中會發生改變,所以接下來有一個問題,見下文。

預處理結束,我們就可以開始查找了。
我們之前提到了,分左右之後找中間的部分,也就是最中間的那個。
查詢肯定不能一次就結束啊,所以我們要用迴圈。
設定變數mid為當前查詢的位置。
這邊有兩種寫法:

1. FOR
for(l=1,r=n;l<=r;)
{
    mid=l+r >> 1
; } 這種寫法用得較少,我個人一般也不這麼寫 2. WHILE l=1,r=n; while(l<=r) { mid=l+r >> 1; }

這裡的 >> 1代表二進位制右移一位,即 /21,佔用時間較少。
注:位運算的優先順序低於加減乘除模
接下來我們要進一步判斷是向左找還是向右找。
設定變數flag代表有沒有找到,初始為false。

if(a[mid]==m)//找到了就彈出迴圈
{
    flag=true;
    break;
}
if(a[mid]>m) r=mid-1;
else l=mid+1;

很好理解吧,當前元素比查詢元素大則我們在查詢元素的右邊,反之亦然。
那麼有同學會注意到,為什麼 lr 要 +1,-1呢?
假定此時 l=2r=3,得 mid=2 ,如果此時不是查詢元素的話,那麼如果當前元素比查詢元素小,那麼我們調整 l 的值。
如果不+1的話會發生什麼呢?l 再次被賦值為2,則進入無限迴圈,所以我們需要+1,-1
則核心程式碼如下:

sort(a+1,a+n+1);
bool flag=false;
int l=1,r=n;
while(l<=r)
{
    int mid=l+r >> 1;
    if(a[mid]==m)
    {
        flag=true;
        break;
    }
    if(a[mid]>m) r=mid-1;
    else l=mid+1;
}

接下來我們舉個栗子。
假定陣列為

1 2 4 5 7 8 9 11

我們來找4這個元素
工程開始:
l=1 , r=8 , mid=4 , a[mid]>4 , r=mid-1=3
l=1 , r=3 , mid=2 , a[mid]<4 , l=mid+1=3
l=3 , r=3 , mid=3 , a[mid]=3 , 退出迴圈
工程結束。
真的很短很快有木有?!
那麼二分查詢可以應用在哪裡呢?
LIS 及 LCS 的 Θ(nlogn) 版就會用到二分查詢。
接下來就是更喜聞樂見喪心病狂的二分答案

EG 2 二分答案

例題:Luogu P2440 木材加工
原題連結
很多老師應該都會拿這道題當入門題吧。
看到這道題,大多數人的想法應該是暴力列舉切段長度吧。
但轉頭一看,原木的長度——
暴力,不存在的!
於是我們便要在答案可能在的範圍中二分查詢我們的答案了。
對這道題來說,答案的範圍就是從1到 最長的原木長度。
於是,我們設 l=1 , r=maxn

好了,範圍解決了,接下來又面臨一個問題了,怎樣判斷答案可不可行呢?
沒辦法了,只能暴力了。
每根原木掃一遍,算出能切多少根,再加起來和 k 比較一下即可

判斷的問題解決了,怎麼判斷下一步搜尋的範圍呢?
很顯然,如果切得太少,那麼長度太長,r=mid-1
如果切得太多,雖然達到了要求,此時我們可以記錄答案,但長度可能太短,可能有更優解,那麼 l=mid+1

程式碼如下:

#include<cstdio>
#include<algorithm>
using namespace std;
int n,k,a[100005],l,r,mid,ans,sum,maxn;
bool Lets_Cut(int m)
{
    sum=0;//設定初值
    for(int i=1;i<=n;i++) sum+=a[i]/m;//計算能切多少
    if(sum>=k) return 1;//如果切夠了返回true
    else return 0;//不夠返回false
}
int main()
{
    scanf("%d %d",&n,&k);
    for(int i=1;i<=n;i++) scanf("%d",&a[i]),maxn=max(maxn,a[i]);
    l=1;
    r=maxn;
    while(l<=r)
    {
        mid=l+r >> 1;
        if(Lets_Cut(mid))
        {
            ans=max(ans,mid);
            l=mid+1;
        }
        else r=mid-1;
    }//全部同上
    printf("%d",ans);
    return 0;
}

結果:
程式碼 C++,0.45KB
耗時/記憶體 0ms, 1738KB

總結

二分是OI中非常重要的一種優化的演算法,可以優化非常巨量的時間複雜度,有很大的必要深入研究與練習

原創 By Venus
寫的不好大佬輕噴