1. 程式人生 > >『二分查詢和二分答案』

『二分查詢和二分答案』

<前言>
分治演算法的一類,也是很重要的一類。


<更新提示>

<第一次更新>


<正文>

二分查詢

引入
問題

給定n個有序的整數,問整數x是否這n個數裡。

分析

使用暴力思想,當然是一個一個查詢判斷。但如果要查詢m個數呢?如果n*m>108呢?我們就必須使用一種新演算法了——二分查詢。

因為n個數是有序的,所以,我們讓第mid=n/2個數與x比較,如果mid=x,那就找到了,如果mid < x,那麼x就一定在區間[mid,n]中,反之,則x在[1,mid]中。這樣,每一次比較,就可以縮小1/2的範圍,我們繼續這樣查詢,就能以log2

n的效率找到x或判定x不存在。我們稱之為二分查詢。

程式碼實現
inline int find(int x)
{
    int left=1,right=n;
    while(left+1<right)
    {
        int mid=(left+right)>>1;
        if(a[mid]==x)return mid;
        if(a[mid]<x)left=mid;
        if(a[mid]>x)right=mid;
    } 
    if(a[left]==x)return left;
    if
(a[right]==x)return right; return -1; }
小結

二分查詢是一種分治的策略,可以用於一個單調的大範圍內的查詢,效率極高。作為列舉,二分查詢也可以找到滿足條件的最大最小值。

例題
1

A-B Problem
自定義測試
題目描述
給出一串數以及一個數字C,要求計算出所有A-B=C的數對的個數。(不同位置的數字一樣的數對算不同的數對)
輸入格式
第一行包括2個非負整數N和C,中間用空格隔開。 第二行有N個整數,中間用空格隔開,作為要求處理的那串數。
輸出格式
輸出一行,表示該串數中包含的所有滿足A-B=C的數對的個數。
樣例資料
input
4 1
1 1 2 3
output
3
資料規模與約定
對於100%的資料,N <= 200000。
所有輸入資料都在int範圍內。
時間限制:
1s
1s
空間限制:
256MB
256MB

分析:

顯然,這是一道二分查詢的模板題,怎麼查詢呢?這回不是精確查找了。對於數列中的每一個數k[i],我們需要找到序列中大於k[i]-c的第一個位置,在找到大於等於k[i]-c的第一個位置,若兩個位置相同,則說明沒有恰為k[i]-c的數,如果兩個位置不同,則說明恰有一個數為k[i]-c,由於數k[i]是已知存在的數,那麼就說明存在這這麼一個數對,統計答案加一。那麼怎麼實現查詢呢,當然是二分查詢。我們可以手動實現,當然,c++內建的函式庫中也為我們準備了函式。
upper_bound(a,a+n,k)意為在陣列a的區間[a,a+n)內二分查詢數k,返回值為第一個大於k的位置。
lower_bound(a,a+n,k)與upper_bound相似,但返回值是第一個大於等於k的位置。
這樣,我們利用這兩個二分函式就能實現快速查找了。
程式碼實現如下:

#include<bits/stdc++.h>
using namespace std;
int k[1<<21]={};
int main()
{
    int n,c;
    cin>>n>>c;
    for(int i=1;i<=n;i++)
    {
        cin>>k[i];
    }
    sort(k+1,k+n+1);
    int ans=0;
    for(int i=1;i<=n;i++)
    {
        ans+=upper_bound(k+1,k+n+1,k[i]-c)-lower_bound(k+1,k+n+1,k[i]-c);
    }
    cout<<ans<<endl;
    return 0;
}
2

mnotes
統計
描述
提交
自定義測試
題目描述
FJ準備教他的奶牛們彈一首歌. 這首歌由N (1 <= N <= 50,000)個音階組成,第i個音階要敲擊 B_i (1<= B_i <= 10,000) 次。奶牛從第0時刻開始彈, 因此他從0時刻到B_1 - 1 時刻都是敲第1個音階,然後他從 B_1 時刻到B_1 + B_2 - 1時刻敲第2個音階,從B_1 + B_2時刻到B_1 + B_2 + B_3- 1時刻敲第3個音階…現在有Q (1 <= Q <= 50,000) 個問題,在時間段區間 [T, T+1)內,奶牛敲的是哪個音階? 其中 T_i (0 <= T_i <= 整首歌的總時刻).
看下面的一首歌,第1個音階持續2個單位時間, 第2個音階持續1個單位時間,第3個音階持續3個單位時間:
這裡寫圖片描述
以下是一些詢問和回答:

詢問 回答的音階
2 2
3 3
4 3
0 1
1 1

輸入格式
第 1 行:兩個整數: N 、Q。
第 2..N+1行: 第i+1行只有一個整數: B_i
第N+2..N+Q+1行: 第N+i+1行只有一個整數: T_i
輸出格式
一行,一個整數,表示答案。
樣例資料
input
3 5
2
1
3
2
3
4
0
1
output
2
3
3
1
1
資料規模與約定
時間限制:
1s
1s
空間限制:
256MB
256MB

分析

對與資料的輸入,我們採用類似字首和統計的方式將其轉化為有序序列,然後對於詢問,直接二分查詢即可。
程式碼實現如下:

#include<bits/stdc++.h>
using namespace std;
int n,q,timee[50080]={},temp,l,r,mid; 
int main()
{
    cin>>n>>q;
    for(int i=1;i<=n;i++)
    {
        cin>>timee[i];
        timee[i]+=timee[i-1];
    }
    for(int i=1;i<=q;i++)
    {
        cin>>temp;
        l=1,r=n;
        while(l+1<r)
        {
            mid=(l+r)/2;
            if(timee[mid]>temp)r=mid;
            else l=mid;
        } 
        if(timee[l]>temp)cout<<l<<endl;
        else cout<<r<<endl;
    }
}

二分答案

引入
問題

使得x^x達到或超過n位數字的最小正整數x是多少?n<=2000000000

分析

對與這種較難求解的問題,我們很難想出較好的解決策略。但是,我們至少知道答案一定在1與2000000000之間,能否轉換二分查詢的思想,對答案進行二分查詢呢?當然是可以的,但在二分查詢中有比較,在二分答案中,我們也需要有比較,我們用檢查函式來實現。如果檢查出的是一個合法的解,那麼我們嘗試找更優的解,如果檢查出的是不合法的解,我們嘗試找合法的解,這就是二分答案的思想。在這題中,我們設計的檢查函式就是x^x的數位是否超過n位,合法返回真,不合法返回假。再二分求解即可。

程式碼實現
#include<bits/stdc++.h>
using namespace std;
int o;
inline bool check(int k)
{
    long long p=k;
    return p*log10(1.0*k)+1>=o;//判斷數位
}
inline int medium()
{
    int l=0,r=2000000000;
    while(l+1<r)
    {
        int mid=(l+r)/2;
        if(check(mid))r=mid;//這是一個合法的解,嘗試縮小範圍,找更優的解
        else l=mid;//解不合法,找合法的解
    }
    if(check(l))return l;
    else return r;
}
int main()
{
    cin>>o;
    cout<<medium()<<endl;
    return 0;
}
小結

二分查詢是一種思維轉換策略,我們可以把一個很難的求解性問題轉換為較為簡單的判斷性問題。
對於二分答案的程式碼實現我們一般都要根據題目要求進行設計。但有這樣的模板:while迴圈查詢時,判斷條件為l+1 < r,縮小範圍時使l,r等於mid,這樣我們最後找到的就是相鄰的兩個l,r,再以是否合法為第一條件,題目需求為第二條件,判斷輸出即可。
二分答案題有很明顯的特徵:求讓最大值最小或最小值最大。

例題
1

路由器安置
題目描述
一條街道安裝WIFI,需要放置M個路由器。整條街道上一共有N戶居民,分佈在一條直線上,每一戶居民必須被至少一臺路由器覆蓋到。現在的問題是所有路由器的覆蓋半徑是一樣的,我們希望用覆蓋半徑儘可能小的路由器來完成任務,因為這樣可以節省成本。
輸入格式
輸入檔案第一行包含兩個整數M和N,以下N行每行一個整數Hi表示該戶居民在街道上相對於某個點的座標。
輸出格式
輸出檔案僅包含一個數,表示最小的覆蓋半徑,保留一位小數。
樣例資料
input
2 3
1
3
10
output
1.0
資料規模與約定
時間限制:
1s
1s
空間限制:
256MB
256MB
註釋
對於60%的資料,有1 ≤ N, M ≤ 100,-1000 ≤ Hi ≤ 1000; 對於100%的資料,有1 ≤ N, M ≤ 100000,-10000000 ≤ Hi ≤ 10000000。

分析

看到巨大的資料,我們考慮二分答案。其中,路由器直徑最小為1,最多為距離最遠的兩戶居民的距離加一。如何設計判斷函式呢?顯然,利用貪心策略即可。我們將每一個路由器放置在能覆蓋到某戶居民的最右端,讓它儘可能靠右,嘗試能否覆蓋到另一戶居民。然後再查詢到下一戶居民即可。以使用的路由器個數判斷方案是否合法。這裡,檢查函式中又用到了二分查詢,我們直接呼叫upper_bound函式即可,減輕了程式碼量。
程式碼實現如下:

#include<bits/stdc++.h>
using namespace std;
int m,n;
int dis[180000]={};
inline int read()
{
    int w=0,x=0;char ch;
    while(!isdigit(ch)){w|=ch=='-';ch=getchar();}
    while(isdigit(ch)){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();}
    return w?-x:x;
}
inline bool check(int d)
{
    int num=1,tot=1;
    while((upper_bound(dis+1,dis+n+1,dis[num]+d)-dis)<=n)
    {
        tot++;
        if(tot>m)return false;
        num=(upper_bound(dis+1,dis+n+1,dis[num]+d)-dis);
    }
    return true;
}
int main()
{
    m=read(),n=read();
    for(int i=1;i<=n;i++)dis[i]=read();
    sort(dis+1,dis+n+1);
    int l=1,r=dis[n]-dis[1]+1;
    while(l<r)
    {
        int mid=(l+r)/2;
        if(check(mid))r=mid;
        else l=mid+1;
    }
    printf("%.1lf",(double)l/2.0);
    return 0;
} 
2

擦護欄
題目描述
Jzyz的所有學生獲得了一個大掃除的機會——去大街擦護欄。 Jz市大街被分成了M段,每段的長度不一定相同。Jzyz一共有N名學生參加勞動,這些學生將分成M組來完成這項工作,因為長度不同,分配的任務量肯定不同,現在,負責這次大掃除的老師想知道,擦護欄長度最多的同學最少必須擦多長的護欄,這樣才能保證儘可能的公平。當然,可以有學生當拉拉隊,不用擦護欄,但是每段護欄必須要擦乾淨。 比如:有5名學生,2段護欄,第一段長度為7,第二段長度為4.可以讓3個人負責擦長度為7的,2個人負責長度為4的,那麼擦第一段的某個人必須要擦長度為3 的護欄,而其他的人擦長度為2 的護欄。這樣就有1位同學必須擦長度為3的護欄。
輸入格式
第一行:兩個整數N和M(1 ≤ N ≤ 10^9) (1 ≤ M ≤ 300 000, M ≤ N)
接下來M行,每行一個整數,表示每段護欄的長度,保證護欄的長度在[1,10^9] 之間。
輸出格式
一個整數,如題目描述,任務量最重的同學擦護欄長度的最小值。
樣例資料
input
7 5
7
1
7
4
4
output
4
資料規模與約定
【資料範圍】 20% 資料保證 N<=30 M<=20 40% 資料保證 N<=10000 M<=1000 100%資料保證 如題目描述
時間限制:
1s
1s
空間限制:
256MB

分析

看到最長的最短,自然想到二分答案。我們對擦護欄的最長長度繼續二分,其中,該長度最長為最長的護欄長度,最短為1。判斷時,我們檢查以該長度為最長長度需要的人數,判斷人數是否足夠,即為是否合法。
程式碼實現如下:

#include<bits/stdc++.h>
using namespace std;
int n,m,len[300080]={},s=0;
inline bool check(int k)
{
    int tot=0;
    for(int i=1;i<=m;i++)
    {
        tot+=len[i]/k;
        if(len[i]%k!=0)tot++;
        if(tot>n)return false; 
    }
    return true;
}
int main()
{
    cin>>n>>m;
    for(int i=1;i<=m;i++)cin>>len[i],s=max(s,len[i]);
    int l=1,r=s;
    while(l+1<r)
    {
        int mid=(l+r)/2;
        if(check(mid))r=mid;
        else l=mid;
    } 
    if(check(l))cout<<l<<endl;
    else cout<<r<<endl;
    return 0;
} 
3

烤魚
題目描述
小x掉到了一個jzyz的窨井裡,這口井很深,沒有人能聽到小x的呼救,悲催的小x必須要等D天后,學校確認他失蹤才會大規模尋找他,而這難熬的D天將是小x這一生最難過的D天。 不過幸運的是小x在井裡得到了N (1 <= N <= 50,000) 條魚,編號1..N.他計劃在接下來的D (1 <= D <= 50,000)天按魚的編號從小到大逐天吃,當然,小x可以一天連續吃多條魚,也可以不吃。   為了不浪費,小x要求魚必須吃完。。 對於第i條魚,小x的能量值會增加Hi(1 <= Hi <= 1,000,000)。小x會在白天吃魚,一旦吃飽,他就會一覺睡到第二天。第二天醒來,她的能量值會變成前一天的能量值的一半(向下取整),小x的能量值是可以累加的。 小x比較注意維護每天能量的平衡,要刻意避免能量的大起大落,於是現在小x想知道,如何安排吃魚,才能保證能量值最小的那個晚上(假設是第X個晚上),第X個晚上的能量值最大。 例如:有5條魚,能量值分別是: (10, 40, 13, 22, 7). 小x按以下方式吃魚,將會得到最優值:
·第幾天
·醒來時能量值
·吃了魚後能量值
·晚上睡覺能量值·
1
0
10+40 
50
2
25
—  
25 
                    
3
12
13  
25
4
12
22  
34
                    
5
17
7   
24
 
可以看出,第1天吃了魚1、魚2,第2天不吃魚,第3天吃了魚3,第4天吃了魚4,第5天吃了魚5。 那麼在這5個晚上,能量值最低的是第5個晚上,能量值是24,所以輸出24。然後你還要輸出第I (1<=i<=N)條魚是小x第幾天吃掉的。
輸入格式
第1行:兩個整數: N 和 D 第2..N+1行: 每行一個整數Hi。
輸出格式
第1行: 一個整數, 在這D個晚上裡,能量值最小的那個晚上(假設是第X個晚上),第X個晚上的能量值最大可以是多少? 第2…..N+1行: 每行一個整數,第 i 行表示第i條魚是第幾天被吃的。
樣例資料
input
5 5
10
40
13
22
7
output
24
1
1
3
4
5
資料規模與約定
40% 保證 N,D<=200
100%資料如題目描述
時間限制:
1s
1s
空間限制:
256MB
256MB

分析

最小的最大,依舊二分。我們先看第一問,利用二分答案求出。判斷函式依據題意模擬即可,判斷的條件是當營養值最小的那個晚上最大為mid時,n條魚是否夠吃,如果夠吃則該數值合法,反之則不合法。對於第二問,我們只需再重複模擬一遍類似檢查函式的過程,將方案輸出即可。注意,這道題的坑點在輸出方案時,如果到了最後一天了還有多餘的魚,需要把那些魚判斷為全是最後一天吃的。即特判最後一天是否又剩下的魚即可。
程式碼實現如下:

#include<bits/stdc++.h>
using namespace std; 
long long n,d,h[50080]={},s=0,ans;
inline long long check(long long k)
{
    long long ate=0,now=0;
    for(long long i=1;i<=d;i++)
    {
        now/=2;
        while(now<k)
        {
            now+=h[++ate];
            if(ate>n)return false;
        }
    }
    return true;
}
int main()
{
    cin>>n>>d;
    for(long long i=1;i<=n;i++)cin>>h[i],s+=h[i];
    long long l=1,r=s;
    while(l+1<r)
    {
        long long mid=(l+r)>>1;
        if(check(mid))l=mid;
        else r=mid;
    }
    if(check(r))ans=r,cout<<r<<endl;
    else ans=l,cout<<l<<endl;
    long long now=0,ate=0;
    for(long long i=1;i<=d;i++)
    {
        now/=2;
        while(now<ans)
        {
            now+=h[++ate];
            cout<<i<<endl;
        }
 }
    if(ate<n)
    {
        for(int i=1;i<=n-ate;i++)
        {
            cout<<d<<endl;
        }
    }
}
4

跳石頭
題目描述
一年一度的“跳石頭”比賽又要開始了!
這項比賽將在一條筆直的河道中進行,河道中分佈著一些巨大岩石。組委會已經選擇好了兩塊岩石作為比賽起點和終點。在起點和終點之間,有 N 塊岩石(不含起點和終 點的岩石)。在比賽過程中,選手們將從起點出發,每一步跳向相鄰的岩石,直至到達 終點。
為了提高比賽難度,組委會計劃移走一些岩石,使得選手們在比賽過程中的最短跳 躍距離儘可能長。由於預算限制,組委會至多從起點和終點之間移走 M 塊岩石(不能 移走起點和終點的岩石)。
輸入格式
輸入檔名為 stone.in。
輸入檔案第一行包含三個整數 L,N,M,分別表示起點到終點的距離,起點和終 點之間的岩石數,以及組委會至多移走的岩石數。
接下來 N 行,每行一個整數,第 i 行的整數 Di(0 < Di < L)表示第 i 塊岩石與 起點的距離。這些岩石按與起點距離從小到大的順序給出,且不會有兩個岩石出現在同 一個位置。
輸出格式
輸出檔名為 stone.out。 輸出檔案只包含一個整數,即最短跳躍距離的最大值。
樣例資料
input
25 5 2
2
11
14
17
21
output
4
資料規模與約定
輸入輸出樣例 1 說明:將與起點距離為 2 和 14 的兩個岩石移走後,最短的跳躍距離為 4(從與起點距離 17 的岩石跳到距離 21 的岩石,或者從距離 21 的岩石跳到終點)。
另:對於 20%的資料,0 ≤ M ≤ N ≤ 10。 對於50%的資料,0 ≤ M ≤ N ≤ 100。
對於 100%的資料,0 ≤ M ≤ N ≤ 50,000,1 ≤ L ≤ 1,000,000,000。
時間限制:
1s
1s
空間限制:
256MB

分析

依舊二分答案,其距離最短為0,最長為l,進行二分,判斷函式根據最短距離最長為mid時所需移去的石頭數是否在m以內進行判斷,在繼續對答案進行二分查詢即可。
程式碼實現如下:

#include<iostream>
using namespace std;
int l,n,m,a[50010],z,y;
int check(int mid)
{
    int sum=0,k=0;
    for(int i=0;i<n;i++)
    if(a[i]-k<mid)
    {

        sum++;
    }
    else k=a[i];
    if(sum>m)return 0;
    else return 1;

}
int main()
{
    cin>>l>>n>>m;
    for(int i=0;i<n;i++)cin>>a[i];
    z=0;y=l;
    while(z<=y)
    {
        int mid=(z+y)/2;
        if(!check(mid))y=mid-1;
        else z=mid+1;
    }
    cout<<y;
}
5

聰明的質監員
題目描述
小 T 是一名質量監督員,最近負責檢驗一批礦產的質量。這批礦產共有 n 個礦石,從 1 到 n 逐一編號,每個礦石都有自己的重量 wi 以及價值 vi。檢驗礦產的流程是:
1、給定 m個區間[Li,Ri];
2、選出一個引數 W;
3、對於一個區間[Li,Ri],計算礦石在這個區間上的檢驗值 Yi :

Y i = j 1 j v j
j∈[Li ,Ri ]且wj ≥W ,j 是礦石編號
這批礦產的檢驗結果 Y為各個區間的檢驗值之和。即:
Y = i = 1 m Y i
若這批礦產的檢驗結果與所給標準值 S 相差太多,就需要再去檢驗另一批礦產。小 T 不想費時間去檢驗另一批礦產,所以他想通過調整引數 W 的值,讓檢驗結果儘可能的靠近 標準值 S,即使得 S-Y的絕對值最小。請你幫忙求出這個最小值。
輸入格式
輸入檔案 qc.in。 第一行包含三個整數 n,m,S,分別表示礦石的個數、區間的個數和標準值。 接下來的 n行,每行 2個整數,中間用空格隔開,第 i+1 行表示 i 號礦石的重量 wi 和價 值 vi 。 接下來的 m行,表示區間,每行 2個整數,中間用空格隔開,第 i+n+1 行表示區間[Li, Ri]的兩個端點 Li 和 Ri。注意:不同區間可能重合或相互重疊。
輸出格式
輸出檔名為 qc.out。 輸出只有一行,包含一個整數,表示所求的最小值。
樣例資料
input
5 3 15
1 5
2 5
3 5
4 5
5 5
1 5
2 4
3 3
output
10
資料規模與約定
【輸入輸出樣例說明】 當 W 選 4 的時候,三個區間上檢驗值分別為 20、5、0,這批礦產的檢驗結果為 25,此 時與標準值 S相差最小為 10。 【資料範圍】
對於 10%的資料,有 1≤n,m≤10;
對於 30%的資料,有 1≤n,m≤500;
對於 50%的資料,有 1≤n,m≤5,000;
對於 70%的資料,有 1≤n,m≤10,000;
對於 100%的資料,有 1≤n,m≤200,000,0

分析

本質還是二分答案。與其他題不同的是,這道我們需要根據題意設計一個求解Y的函式,而非檢查函式,再通過與S比較大小來二分答案。而求解函式需要用到字首和的累加求解法以減輕時間複雜度,資料型別要開longlong,就可以解決這道題。
程式碼實現如下:

#include<bits/stdc++.h>
using namespace std;
long long n,m,S,Left,Right,mid,ans=9999999999999999LL,tot=0,Y;
struct stone{long long value,weight;}Stone[200080]={};
struct interval{long long begin,end;}Interval[200080]={};
struct sum{long long num,s;}Sum[200080]={};
inline void input()
{
    cin>>n>>m>>S;
    for(int i=1;i<=n;i++)cin>>Stone[i].weight>>Stone[i].value,tot+=Stone[i].weight;
    for(int i=1;i<=m;i++)cin>>Interval[i].begin>>Interval[i].end;
}
inline long long solve(long long mid)
{
    for(int i=1;i<=n;i++)
    {
        Sum[i]=Sum[i-1];
        if(Stone[i].weight>=mid)
        {
            Sum[i].num++;
            Sum[i].s+=Stone[i].value;
        }
    }
    long long result=0;
    for(int i=1;i<=m;i++)
    {
        result+=(Sum[Interval[i].end].num-Sum[Interval[i].begin-1].num)*(Sum[Interval[i].end].s-Sum[Interval[i].begin-1].s);
    }
    return result;
}
inline void find()
{
    Left=1;
    Right=tot;
    while(Left+1<Right)
    {
        mid=(Left+Right)>>1;
        memset(Sum,0,sizeof(Sum));
        Y=solve(mid);
        if(Y<S)Right=mid;
        else Left=mid;
    }
    Y=solve(Left);
    if(abs(Y-S)<ans)ans=abs(Y-S);
    Y=solve(Right);
    if(abs(Y-S)<ans)ans=abs(Y-S);
    cout<<ans<<endl;
}
int main()
{
    input();
    find();
    return 0;
}

<後記>


<廢話>