二分答案经典例题(1) 整数域的二分答案

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/guapi2333/article/details/83409903

什么时候我们要二分答案?

答:当答案具有单调性时(这不是废话吗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;
}

猜你喜欢

转载自blog.csdn.net/guapi2333/article/details/83409903