二分答案經典例題(1) 整數域的二分答案
什麼時候我們要二分答案?
答:當答案具有單調性時(這不是廢話嗎emmm)
來看一道最經典的例題:
https://www.luogu.org/problemnew/show/P1182
Problem1:對於給定的一個長度為的正整數數列,現要將其分成段,並要求每段連續,且每段和的最大值最小。
資料範圍:
考慮二分答案:
我們假設每段和的最大值為內的某個值,顯然答案要求的。
單調性:當越大時,選取的合法的段的長度就越長,需要分成的段的個數就越少。當越小時,選取的合法的段的長度就越短,需要分成的段的個數就越多。
那麼假設當前可以確定我們求的每段和的最大值的最小值就在內。取:
1、如果取時,我們可以將原數列劃成段,那麼根據剛才說的單調性,是合法的,此時時也一定合法(能將將數列劃成段),但我們要儘量最小化,所以時一定合法,但由於當前是合法的,故時顯然,其答案不會比 時更優,所以答案已不可能在內,故取。
2、若當前時不合法,那麼同(1)理,取。
初始化:
核心程式碼:
while(l+1<r)
{
mid=(l+r)>>1;
flag=check(mid);
if(!flag) l=mid;
if(flag) r=mid;
}
這個程式碼還是蠻有講究的,具體表現在為什麼while迴圈的終止條件是?
可能有人會疑惑為什麼不去。那我們現在假設為迴圈的終止條件,當前,此時應取。放在這個題裡面說:如果當前檢驗時無法將數列劃分成段,此時應取,這樣新的,還和剛才的一樣。這時你又檢驗了一遍時合不合法,(前面說了不合法),然後你又取,如此周而復始,你會驚奇的發現你的程式碼死循了。
所以,我們設定終止條件為,保證退出迴圈時
注意:由於二分的區間內包含的值都有可能是答案,所以在退出迴圈後對和進行是否合法的檢驗。
對於這個題來講,答案越小越好,所以退出while迴圈後的後續處理這麼寫:
for(ri i=l;i<=r;i++)
if(check(i)) { ans=i; break; }
check函式怎麼寫?
因為要求劃分的段是連續的,所以我們從開始劃分。
bool check(int maxn)
{
int now=0,cnt=1;
for(ri i=1;i<=n;i++)
{
if(a[i]>maxn) return 0;單個元素值>mid時肯定不合法
if(now+a[i]<=maxn) { now+=a[i]; continue; }//此時說明不需要劃分新段
now=a[i],cnt++;
}
if(cnt<=m) return 1;//當前x=mid合法時返回1,否則返回0
return 0;
}
程式碼:
#include<cstdio>
#include<iostream>
#define ri register int
using namespace std;
const int MAXN=100020;
int n,m,sum,a[MAXN],l,r,mid,flag,ans;
bool check(int maxn)
{
int now=0,cnt=1;
for(ri i=1;i<=n;i++)
{
if(a[i]>maxn) return 0;
if(now+a[i]<=maxn) { now+=a[i]; continue; }
now=a[i],cnt++;
}
if(cnt<=m) return 1;
return 0;
}
int main()
{
scanf("%d%d",&n,&m);
for(ri i=1;i<=n;i++) { scanf("%d",&a[i]); sum+=a[i]; }
l=0,r=sum+1;
while(l+1<r)
{
mid=(l+r)>>1;
flag=check(mid);
if(!flag) l=mid;
if(flag) r=mid;
}
for(ri i=l;i<=r;i++)
if(check(i)) { ans=i; break; }
cout<<ans;
return 0;
}
Problem2:陶陶在地上丟了A個瓶蓋,這A個瓶蓋丟在一條直線上,現在他想從這些瓶蓋裡找出B個(B<=A<=100000),使得距離最近的2個距離最大,他想知道最大可以達到多少。
單調性:選取的瓶蓋的最近距離越大,能選取的瓶蓋個數就越少。
二分距離最近的瓶蓋之間的距離。顯然,要求的最近的瓶蓋之間的距離越大,滿足條件的瓶蓋就越少。這便是單調性。
check函式的寫法:貪心,從左往右能匹配就匹配。
Code:
#include<cstdio>
#include<iostream>
#include<algorithm>
#define ri register int
using namespace std;
const int MAXN=100020;
int n,m,a[MAXN],l,r,mid,ans;
bool check(int minn)
{
int cnt=1,now=a[1],flag1=0,flag2=0;
for(ri i=2;i<=n;i++)//從左往右貪心一遍
if(a[i]-now>=minn) cnt++,now=a[i];
if(cnt>=m) flag1=1;
cnt=1,now=a[n];
for(ri i=n-1;i>=1;i--)//從右往左貪心一遍
if(now-a[i]>=minn) cnt++,now=a[i];
if(cnt>=m) flag2=1;
if(flag1||flag2) return 1;
return 0;
}
int main()
{
scanf("%d%d",&n,&m);
for(ri i=1;i<=n;i++) scanf("%d",&a[i]);
sort(a+1,a+n+1);
l=0,r=a[n]-a[1]+1;
while(l+1<r)
{
mid=(l+r)>>1;
if(check(mid)) l=mid;
else r=mid;
}
for(ri i=r;i>=l;i--)
if(check(i)) { ans=i; break; }
cout<<ans;
return 0;
}