1. 程式人生 > >二分答案經典例題(1) 整數域的二分答案

二分答案經典例題(1) 整數域的二分答案

什麼時候我們要二分答案?

答:當答案具有單調性時(這不是廢話嗎emmm)

來看一道最經典的例題:

https://www.luogu.org/problemnew/show/P1182

Problem1:對於給定的一個長度為\small N的正整數數列A,現要將其分成\small M(M\leq N)段,並要求每段連續,且每段和的最大值\small X最小。

資料範圍:\small N\leq 10^{5},M\leq N,A{}_{i}>0,\sum A{}_{i}\leq 10^{9}

考慮二分答案:

我們假設每段和的最大值\small X\small [l,r]內的某個值,顯然答案要求的\small X\in [0,sum]

單調性:當\small X越大時,選取的合法的段的長度就越長,需要分成的段的個數就越少。當\small X越小時,選取的合法的段的長度就越短,需要分成的段的個數就越多。

那麼假設當前可以確定我們求的每段和的最大值\small X的最小值就在\small [l,r]內。取\small mid=\frac{l+r}{2}

1、如果取\small X=mid時,我們可以將原數列劃成\small \leq M段,那麼根據剛才說的單調性,\small mid是合法的,此時\small X\in (mid,r]時也一定合法(能將將數列劃成\small \geq M段),但我們要儘量最小化\small X,所以\small X\in [mid,r]時一定合法,但由於當前\small X=mid是合法的,故\small X\in [mid,r]時顯然\small \geq mid,其答案不會比\small X=mid 時更優,所以答案已不可能在\small (mid,r]內,故取\small r=mid

2、若當前\small X=mid時不合法,那麼同(1)理,取\small l=mid

初始化:\small l=0,r=sum(A_{i})+1

核心程式碼:

while(l+1<r)
{
    mid=(l+r)>>1;
    flag=check(mid);
    if(!flag)	l=mid;
    if(flag)	r=mid;
}

這個程式碼還是蠻有講究的,具體表現在為什麼while迴圈的終止條件是\small l+1<r?

可能有人會疑惑為什麼不去\small l<r。那我們現在假設\small l<r為迴圈的終止條件,當前\small l=5,r=6,此時應取\small mid=(5+6)/2=5。放在這個題裡面說:如果當前檢驗\small X=5時無法將數列劃分成\small \leq M段,此時應取\small l=mid=5,這樣新的\small l=5,r=6,還和剛才的\small l,r一樣。這時你又檢驗了一遍\small X=5時合不合法,(前面說了不合法),然後你又取\small l=5,如此周而復始,你會驚奇的發現你的程式碼死循了。

所以,我們設定終止條件為\small l+1<r,保證退出迴圈時\small l+1=r

注意:由於二分的區間\small [l,r]內包含的值都有可能是答案,所以在退出迴圈後對\small l\small l+1(r)進行是否合法的檢驗。

對於這個題來講,答案越小越好,所以退出while迴圈後的後續處理這麼寫:

for(ri i=l;i<=r;i++)
    if(check(i))	{ ans=i; break; }

check函式怎麼寫?

因為要求劃分的段是連續的,所以我們從\small A{}_{1}開始劃分。

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;
}