[總結]二分法(二分查找)



一、關於二分法

二分法是一種很普通卻又很重要的算法。二分法能為我們解題時提供很大的幫助。

1. 使用前提

二分法的適用條件是序列具有二分性,也就是單調性。當序列具有二分性,這時我們不斷枚舉區間中點才能判斷這個值是否題設條件。
當題目中出現諸如最大值的最小最小值的最大的問題時,答案具有二分性。

2. 分類

從二分的對象來分類,我們既可以二分最終的答案,我們也可以二分進行判斷。
從二分的類型來分類,可以分為整數域上的二分,以及實數域上的二分

3. 易錯點

二分法簡單易寫,但是卻很容易寫錯。我們有很多方法實現二分,而其中的細節地方需要仔細考慮。
對於整數域上的二分:
我們需要注意終止條件,左右區間位置的變化,避免錯過答案或造成死循環。
對於實數域上的二分:
我們需要注意精度的控制。
建議自己形成固定的代碼模型,避免造成不必要的錯誤。

4. 二分法的延伸

C++ STL中的lower_boundupper_bound也可以解決實現在一個序列中二分查找某個整數k的后繼。
二分法能夠解決單調問題,進一步地,我們可以擴展二分法至三分法。此時三分法可以解決單峰函數的極值問題。

二、整數域上的二分

1. 模板

在這里給出一種常見的模板:

while(l<=r){
	int mid=(l+r)>>1;
	if(check(mid)){
		ans=mid;
		r=mid-1;
	}
	else l=mid+1;
}

三、實數域上的二分

1. 模板

實數域上的二分相對簡單,只要r-l到達我們所需的精度即可。

#define eps 1e-5
while(r-l>eps){
    double mid=(l+r)/2;
    if(check(mid)) r=mid;
    else l=mid;
}

當我們不確定精度的時候,我們可以采用循環固定次數的形式進行計算。一般這種方式得到的結果的精度比設置的eps更高:

for(int i=1;i<=100;i++){
    double mid=(l+r)/2;
    if(check(mid)) r=mid;
    else  l=mid;
}

四、練習

例1:#9100055「一本通 1.2 例 1」憤怒的牛 / SP297 AGGRCOW - Aggressive cows / P1316 丟瓶蓋

分析:
很基礎的二分,每次二分牛的間隔,如果能放下這c頭牛,那么繼續擴大這個距離,否則縮小這個距離,直到找到答案。
代碼如下:

#include<bits/stdc++.h>
using namespace std;
int f[1000050],n,c,rem;
int judge(int x){
    int num=0;
    int temp=f[1]; 
    for(int i=2;i<=n;i++){
        if(f[i]-temp<x) num++;
        else temp=f[i];
        if(num>rem) return 0;
    }
    return 1;
}
int main()
{
	scanf("%d%d",&n,&c);
	for(int i=1;i<=n;i++) scanf("%d",&f[i]);
	sort(f+1,f+n+1);
	rem=n-c;
	int maxn=0;
	int l=1,r=f[n]-f[1];
	while(l+1<r){
    	int mid=(l+r)/2;
    	if(judge(mid)) l=mid; 
    	else r=mid;
  	}
  	printf("%d\n",l);
	return 0;
}

例2:P1661 擴散

分析:
並查集+二分答案。二分枚舉形成一個連通塊的時間,每次使用並查集統計,如果最后集合的數量大於1,那么移動左區間,否則移動右區間。注意兩個點都會擴張,因此單位時間會走雙倍的距離。
代碼如下:

#include<bits/stdc++.h>
#define N 100010
using namespace std;
int sx[N],sy[N],pre[N],n;
int Find(int x){
	return (pre[x]==x)? x:Find(pre[x]);
}
int check(int mid){
	for(int i=1;i<=n;i++) pre[i]=i;
    for(int i=1;i<=n;i++){
        for(int j=i+1;j<=n;j++){
            int dis=abs(sx[i]-sx[j])+abs(sy[i]-sy[j]);
            if(mid*2>=dis){
                int fi=Find(i);
                int fj=Find(j);
                if(fi!=fj) pre[fi]=fj;
        	}
        }
    }
    int cnt=0;
    for(int i=1;i<=n;i++) if(pre[i]==i) cnt++;
	return (cnt==1)? 1:0;
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
		scanf("%d%d",&sx[i],&sy[i]);
    int l=0,r=1e9,ans=0;
    while(l<=r){
        int mid=(l+r)>>1;
        if(check(mid)){
            r=mid-1;
            ans=mid;
        }
        else l=mid+1;
    }
    printf("%d",ans);
    return 0;
}

例3:P1182 數列分段 Section II

分析:
二分枚舉每段總和為mid時是否可行,分了超過m段就更新左區間,否則更新右區間。
代碼如下:

#include<bits/stdc++.h>
using namespace std;
int a[100010];
int n,m,ans,l,r;
int judge(int mid){
	int sum=0,cnt=1;
	for(int i=1;i<=n;i++){
		if(sum+a[i]<=mid)
			sum+=a[i];
		else{
			sum=a[i];
			cnt++;
		}
	}
	return (cnt<=m)?1:0;
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
		l=max(l,a[i]);
		r+=a[i];
	}
	while(l<=r){
		int mid=(l+r)>>1;
		if(judge(mid)){
			ans=mid;
			r=mid-1;
		}
		else l=mid+1;
	}
	printf("%d",ans);
    return 0;
}

例4:POJ 2018 Best Cow Fences

分析:
實數域上的二分。因為平均值只是描述數與數的離散關系,所以我們同時加或減對整個序列的平均值都沒有影響。所以我們對序列減去平均值后,問題化為存不存在這樣的序列使得區間和大於0。
我們在\(O(N)\)復雜度內使用前綴和做減法處理出全序列中最大的一段子序列,如果此時序列和小於0,那么我們枚舉的平均值過大,因此縮進右區間,反之同理。

#include<bits/stdc++.h>
#define N 100010
#define INF 1e10
using namespace std;
double a[N],b[N],sum[N];
int main()
{
	int n,len;
	scanf("%d%d",&n,&len);
	for(int i=1;i<=n;i++) scanf("%lf",&a[i]);
	double l=-1e6,r=1e6;
	double dlt=1e-5;
	while(r-l>dlt){
		double mid=(l+r)/2;
		for(int i=1;i<=n;i++) b[i]=a[i]-mid;//削去平均值 
		for(int i=1;i<=n;i++) sum[i]=sum[i-1]+b[i];//求前綴和 
		double ans=-INF,temp=INF;
		for(int i=len;i<=n;i++){
			temp=min(temp,sum[i-len]);//因為長度大於等於L,所以確定一個min左端點 
			ans=max(ans,sum[i]-temp);
		}
		if(ans>=0) l=mid;//可以達到該平均值 
		else r=mid;
	}
	printf("%d",int(r*1000));
	return 0;
}

例5:CF670C Cinema

分析:
貪心思想,二分枚舉每場電影能聽懂配音的人數以及看懂字幕的人數,首先滿足聽懂配音人數,其次滿足看懂字幕的人數。

#include<bits/stdc++.h>
#define N 200010
using namespace std;
inline void read(int &x){
	x=0;int flag=1;char ch=getchar();
	while(ch<'0'||ch>'9'){
		if(ch=='-') flag=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9'){
		x=(x<<1)+(x<<3)+ch-'0';
		ch=getchar();
	}
	x*=flag;
}
int n,m,a[N],b[N],v,pow1,pow2,last1,last2;
int ans=1;
int main()
{
	read(n);
	for(int i=1;i<=n;i++) read(a[i]);
	sort(a+1,a+n+1);read(m);
	for(int i=1;i<=m;i++) read(b[i]);
	for(int i=1;i<=m;i++){
		read(v);
		int pow1=(upper_bound(a+1,a+n+1,b[i])-a-1)-(lower_bound(a+1,a+n+1,b[i])-a-1);
		int pow2=(upper_bound(a+1,a+n+1,v)-a-1)-(lower_bound(a+1,a+n+1,v)-a-1);
		if(pow1>last1||(pow1==last1&&pow2>last2)) last1=pow1,last2=pow2,ans=i;
	}
	printf("%d",ans);
	return 0;
}

例6:POJ3579 Median

代碼如下:

#include<iostream>
#include<cstdlib>
#include<cstdio>
#include<algorithm>
const int N=1e6;
using namespace std;
int a[N],n,m,ans;
int check(int val)
{
    int cnt=0;
    for(int i=1;i<=n;i++)
		cnt+=n-(lower_bound(a+1,a+n+1,a[i]+val)-a-1);
    if(cnt>m) return 1;
    else return 0;
}
int main()
{
    while(~scanf("%d",&n))
    {
		m=n*(n-1)/4;
		ans=-1;
		for(int i=1;i<=n;i++)
			scanf("%d",&a[i]);
		sort(a+1,a+n+1);
		int l=1,r=a[n]-a[1];
		while(l<=r){
	    	int mid=(l+r)>>1;
	    	if(check(mid)){
				ans=mid;
				l=mid+1;
	    	}
	    	else r=mid-1;
		}
		printf("%d\n",ans);
    }
	return 0;
}

例7:P1083 借教室

分析:
一道很好的思維題,運用了差分的思想。我們在第i天借了k個教室時,在這個時間節點累加這k個教室,在第j天歸還的時候再減去。這樣我們就能知道任意一天借出教室的數量。我們不斷二分這個不能滿足的日期,如果最后結果為m,那么全都能滿足。否則二分終止的位置就是不能滿足的日期。
代碼如下:

#include<bits/stdc++.h>
using namespace std;
int num[1000010],day[1000010];
int m,n,l[1000010],r[1000010],req[1000010];
int judge(int mid){
	memset(day,0,sizeof(day));
	for(int i=1;i<=mid;i++){
		day[l[i]]+=req[i];
		day[r[i]+1]-=req[i];
	}
	if(day[1]>num[1]) return 0;
	for(int i=2;i<=n;i++){
		day[i]+=day[i-1];
		if(day[i]>num[i]) return 0;
	}
	return 1;
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) scanf("%d",&num[i]);
	for(int i=1;i<=m;i++) scanf("%d%d%d",&req[i],&l[i],&r[i]);
	int L=1,R=m,ans=0;
	while(L<=R){
		int mid=(L+R)>>1;
		if(judge(mid)){
			L=mid+1;
		}
		else R=mid-1,ans=mid;
	}
	if(R!=m) printf("-1\n%d",ans);
	else printf("0");
	return 0;
}

例8: WILL吃桃

【 題目描述】
Will 很喜歡吃桃, 某天 Will 來到了一片森林, 森林中有 N 顆桃樹, 依次編號為 1,2,„,N.每棵樹上有數量不等的桃子。 某些桃樹之間有單向通行的小路, 且路徑不會形成環, 通過每條小路的時間也不一定相同。 現在, Will 提着一個最多可以容納 K 個桃子的籃子, 從編號為1 的桃樹出發, 走過若干條小路之后來到編號為 N 的桃樹。 當 Will 在路上走的時候, 每走 1分鍾, 他會從籃子中拿出一個桃子來吃掉( 如果籃子中還有桃子的話, 如果籃子中沒有桃子的話那就沒得吃了!)。 每到一棵桃樹( 包括起點和終點), 他會把這棵桃樹上的所有桃子摘下來放入籃子中。 現在你的問題是: 求 K 的最小值, 使得 Will 能夠不浪費任何桃子( 每到一棵桃樹, 這棵樹上的所有桃子都必須被裝入籃子中)。

【 輸入格式】
輸入文件第一行兩個整數, N,m, 分別表示桃樹的數量以及連接桃樹的小路的數量。
接下來一行 N 個用空格隔開的整數, 分別表示每一顆桃樹上的桃子的數量。
接下來 m 行, 每行 3 個用空格隔開的整數, a,b,c, 表示有一條小路能夠從桃樹 a 走到桃樹 b,( 注意小路一定是單向的), 走過這條小路所需要的時間是 c 分鍾。從任意一棵桃樹出發, Will 不可能沿着小路走若干條路之后重新回到這棵桃樹。( 給出的圖是一個有向無環圖。) 數據保證 Will 一定能夠從桃樹 1 走到桃樹 N。

【 輸出格式】
輸出文件有且僅有一行, 一個整數, 表示 K 的最小值

【 輸入樣例】
3 3
5 1 6
1 3 1
1 2 4
2 3 5
【 輸出樣例】
6
【 數據規模】
對於 30%的數據: 3≤N≤10; m≤20;
對於 60%的數據: 3≤N≤1,000; m≤10,000;
對於 100%的數據: 3≤N≤10,000; 3≤m≤30,000; 所有其他數據都不超過 10000;

分析:
題目要求求出籃子容量的最小值K,我們很容易知道如果籃子容量小於K無法滿足,如果容量大於K則會有剩余的空間而不是最優的答案。因此K的取值具有二分性,我們二分這個K並跑最短路(以籃子中的桃子作為權值),如果最終籃中的桃子數少於mid,那么縮小右區間,否則縮小左區間。

具體詳見代碼:

#include<bits/stdc++.h> 
#define N 100010
#define INF 0x3f3f3f3f
using namespace std;
int n,m,tot;
int first[N],nxt[N],go[N],cost[N],fil[N];//fil每個節點裝入桃子的數量 
int vis[N],dist[N];
inline void add_edge(int u,int v,int w){
	nxt[++tot]=first[u];
	first[u]=tot;
	go[tot]=v;
	cost[tot]=w;
}
int check(int mid){
	queue<int> q;//最短路 
	for(int i=1;i<=n;i++) vis[i]=0,dist[i]=INF;
	vis[1]=1;
	dist[1]=fil[1];
	q.push(1);
	while(!q.empty())
	{
		int u=q.front();
		q.pop();
		vis[u]=0;
		for(int e=first[u];e;e=nxt[e]){
			int v=go[e],w=cost[e];
			int add=fil[v];
			int rest=(dist[u]-w>0)?(dist[u]-w):0;//可能會桃子不夠吃的情況 
			if(add+rest>mid) continue;//裝不下,不能選這條道 
			if(add+rest<dist[v]){//松弛 
				dist[v]=add+rest;
				if(!vis[v]){
					q.push(v);
					vis[v]=1;
				}
			}
		}
	}
	return (dist[n]<=mid)? 1:0;
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
		scanf("%d",&fil[i]);
	for(int i=1;i<=m;i++){
		int u,v,w;
		scanf("%d%d%d",&u,&v,&w);
		add_edge(u,v,w);
	}
	int l=fil[1],r=1e8,ans;
	while(l<=r){
		int mid=(l+r)>>1;
		if(check(mid)){
			ans=mid;
			r=mid-1;
		}
		else l=mid+1;
	}
	printf("%d",ans);
	return 0;
}

例9: 【codevs 3342】綠色通道Green Passage

題目描述 Description
《思遠高考綠色通道》(Green Passage, GP)是唐山一中常用的練習冊之一,其題量之大深受lsz等許多oiers的痛恨,其中又以數學綠色通道為最。2007年某月某日,soon-if (數學課代表),又一次宣布收這本作業,而lsz還一點也沒有寫……
高二數學《綠色通道》總共有n道題目要寫(其實是抄),編號1..n,抄每道題所花時間不一樣,抄第i題要花a[i]分鍾。由於lsz還要准備NOIP,顯然不能成天寫綠色通道。lsz決定只用不超過t分鍾時間抄這個,因此必然有空着的題。每道題要么不寫,要么抄完,不能寫一半。一段連續的空題稱為一個空題段,它的長度就是所包含的題目數。這樣應付自然會引起馬老師的憤怒。馬老師發怒的程度(簡稱發怒度)等於最長的空題段長度。
現在,lsz想知道他在這t分鍾內寫哪些題,才能夠盡量降低馬老師的發怒度。由於lsz很聰明,你只要告訴他發怒度的數值就可以了,不需輸出方案。(快樂融化:那么lsz怎么不自己寫程序?lsz:我還在抄別的科目的作業……)

輸入描述 Input Description
第一行為兩個整數n,t,代表共有n道題目,t分鍾時間。
以下一行,為n個整數,依次為a[1], a[2],... a[n],意義如上所述。

輸出描述 Output Description
僅一行,一個整數w,為最低的發怒度。

樣例輸入 Sample Input
17 11
6 4 5 2 5 3 4 5 2 3 4 5 2 3 6 3 5

樣例輸出 Sample Output
3

數據范圍及提示 Data Size & Hint
60%數據 n<=2000
100%數據 0<n<=50000,0<a[i]<=3000,0<t<=100000000

分析:
讓你求出最小的發怒值。本題繼承上一道題的思想,可以確定發怒值具有二分性,那么如何寫check函數呢?
我們很容易想到動態規划,由於每個作業的區間存在重疊關系,因此可以使用單調隊列優化。設\(f(i)\)數組表示在完成作業\(i\)時要花費的最小總時間,我們維護一個單調上升的隊列,當掃描到的數所花的時間比隊尾大,那么不斷刪去隊尾並插入這個數來保證每次隊頭的元素的值最小;在取隊頭前需要檢查隊頭元素是否超出區間,超出的元素需要刪去。
Code:

#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
#define N 50010
using namespace std;
int n,t,a[N],f[N],q[N];
int check(int val)
{
    memset(f,0x3f,sizeof(f));
    memset(q,0,sizeof(q));
	f[0]=0;
    int head=0,tail=0;
    tail++;
    for(int i=1;i<=n;i++)//單調上升隊列 
    {
        while(head<=tail&&q[head]<i-val-1)//(多減一位,相當於i與選的值中間差了val(即mid))
			head++;//超出范圍的元素要刪除 
        f[i]=f[q[head]]+a[i];//更新時間 
        while(head<=tail&&f[q[tail]]>=f[i]) tail--;//刪除不是最優的隊尾 
        q[++tail]=i;//加入新的位置 
    }
    int ans=INF;
    for(int i=n-val-1;i<=n;i++)
        ans=min(ans,f[i]);
    //若有時間小於t,則該憤怒值枚舉d大了 
    if(ans<=t) return 1;//更新右區間 
    return 0;
}
int main()
{
	scanf("%d%d",&n,&t);
	for(int i=1;i<=n;i++)
		scanf("%d",&a[i]);
	int l=0,r=n,ans=0;
	while(l<=r){
		int mid=(l+r)>>1;
		if(check(mid)){
			ans=mid;
			r=mid-1;
		}
		else l=mid+1;
	}
	printf("%d",ans);
	return 0;
}

例10: P1314 聰明的質監員

前綴和優化,check函數:“從Li到Ri,中間的所有w值超過W的項的個數乘上這些礦石的v的和”。
我們二分枚舉參數W,並求出最終的Y,尋找\(min(abs(S-Y))\)意味着S與Y的距離最近,因此只要Y大於S就更新l,否則更新r,這樣就可以向S不斷逼近。
Code:

#include<bits/stdc++.h>
#define N 200010
#define INF 0x7f7f7f7f
#define ll long long
using namespace std;
ll res,n,m,s,w[N],val[N],y,a[N],b[N];
ll sumn[N],sumv[N],L=INF,R=0;
inline bool judge(ll mid){
	y=0;
	memset(sumv,0,sizeof(sumv));//滿足條件礦石的價值
	memset(sumn,0,sizeof(sumn));//滿足條件礦石的個數
	for(int i=1;i<=n;i++){
		sumn[i]=sumn[i-1],sumv[i]=sumv[i-1];
		if(w[i]>=mid) sumv[i]+=val[i],sumn[i]++;//符合條件
	}
	for(int i=1;i<=m;i++)
		y+=(sumn[b[i]]-sumn[a[i]-1])*(sumv[b[i]]-sumv[a[i]-1]);//求出Y
	res=(ll)abs(y-s);//求出當前答案
	if(y>s) return 1;//W可以更小一些
	else return 0;//W可以更大一些
}
int main()
{
	scanf("%lld%lld%lld",&n,&m,&s);
	for(int i=1;i<=n;i++){
		scanf("%lld%lld",&w[i],&val[i]);
		L=min(w[i],L);R=max(w[i],R); 
	}
	for(int i=1;i<=m;i++) scanf("%lld%lld",&a[i],&b[i]);
	ll l=0,r=R+2,ans=0x3f3f3f3f3f3f3f3f;
	while(l<=r){
		ll mid=(l+r)>>1;
		if(judge(mid)) l=mid+1;
		else r=mid-1;
		ans=min(ans,res);
	}
	printf("%lld",ans);
	return 0;
}

例11: P1281 書的復制

二分枚舉完成的時間,求出最優時間后用遞歸求出最終每個人負責的范圍。
細節見代碼。
Code:

#include<bits/stdc++.h>
using namespace std;
int n,k,a[600],l,r,ans,cnt;
inline int check(int time){
	int per=0;cnt=1;//初始化
	for(int i=1;i<=n;i++){
	    if(a[i]>time) return 0;//不可能完成該任務
		if(per+a[i]<=time) per+=a[i];//貪心,即讓每個人在限制時間內盡量多地抄寫
		else cnt++,per=a[i];//超出時間讓下一個人做
	}
	return cnt<=k;//是否滿足限制人數
}
inline void print(int x,int y){
	int per=0;
	for(int i=y;i>=x;i--){
		if(per+a[i]>ans){
			print(x,i);//繼續划分1~i
			printf("%d %d\n",i+1,y);//i+1~y是一段
			return;
		}
		per+=a[i];
	}
	printf("%d %d\n",x,y);
	return;
}
int main()
{
	scanf("%d%d",&n,&k);
	if(!n) return 0;//數據會出現n=0,k=0的情況 
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
		r+=a[i];
	}
	while(l<=r){//二分求出最小時間
		int mid=(l+r)>>1;
		if(check(mid)){
			r=mid-1;
			ans=mid;
		}
		else l=mid+1;
	}
	print(1,n);
	return 0;
}

pic.png


免責聲明!

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



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