首页 > 代码库 > 二分答案入门乱讲

二分答案入门乱讲

1.关于二分答案

如果reader没有学过二分,那么我建议您把这个网站关掉。不是我有偏见或者什么,看这篇文章对不了解二分的人来说没有好处。

对 于一些问题,它的解满足单调性,即如果x满足条件,则对于任意的 i ( 1<=i<=x) 或 (x <=i <=n) (假设1和n是答案的上下界)都会满足条件。一般遇上这种问题,我们就可以用二分答案来加快解决。这种问题常常有关键语句:使最大......最小。

对于上面的问题,在没学二分答案的时候,我们是这么写的:(假设答案是上界)

for(int i=1;i<=n;++i)
  if(!check(i))
  {
    Ans=i-1;
    break;
  }

枚举答案,进行检查。出现第一个不合法的解,答案就是它前面那个值。但这样看来,未免太慢。我们花了很长时间来检查每一个答案。如果答案是10000,我们就会做很多无用功。或者说我们枚举1000是正确的,那么前999个都可以看成白枚举了,很浪费。

那么我们该如何尽可能的少做这些无用功呢?

我 们来设想一下。同样假设答案是上界,如果我们check了10000,发现它是满足解的,那么答案肯定不小于10000。如果我们又check了 20000,发现它是满足解的,那么10000~20000内的数我们都不用枚举。又或者20000是不满足解的,那么答案就在10000~20000的 左闭右开区间内。这个时候我们如果”恰当地“check 15000,答案的范围会进一步缩小。

看到这里我们大概都会想到二分了。一步一步地缩小答案范围最终出解。

身边的巴拉拉2016对我说:二分答案的板子大家都会啊。

 

int L=1,R=n;    
while(L<=R)
{
    int mid=(L+R)>>1;
    if(check(mid))L=mid+1;
    else R=mid-1;
}
printf("%d",L);

是啊,zz的板子谁都会。重点就在check函数上面。我们要使用时间复杂度最优的check来写。怎么写出最好的方法呢?分析题目,优化算法,然后大胆猜想不用证明。多做题目,积累经验。然后因为我走歪了,check一向非主流,但莫名好用啊(其实check只要有不合法就可以立即弹出,复杂度可以降到玄学级)。

2.例题

下面分享一些题目,都是NOIP里面的水题,都是可以一遍AC的。

1.NOIP2015 河中跳房子

最大距离最小,一看就知道是二分答案。

那么该如何写check呢?

我们可以check当最大距离为mid时,所需要搬走的石头的个数。方法就是一个小小的贪心——能不拿走就不拿走,能少拿走就少拿走,然后一次check在O(n)的时间内跑过。总时间复杂度是O(nlogn)。

#include    <iostream>
#include    <cstdio>
#include    <cstdlib>
#include    <algorithm>
int L,M,N,pla[50100];
int gi()
{
    int x=0;char ch=getchar();
    while(ch>9 || ch<0)ch=getchar();
    while(ch>/ && ch<:)x=(x<<1)+(x<<3)+ch-48,ch=getchar();
    return x;
}
int check(int x)
{
    int Ans=0,sta=0;
    for(int i=1;i<=N;++i)
    {
        while(pla[i]-sta<x && i<=N)
        {
            Ans++;i++;
        }
        sta=pla[i];
    }
    return Ans;
}
using namespace std;
int main()
{
    L=gi();N=gi();M=gi();pla[0]=0;
    for(int i=1;i<=N;++i)pla[i]=gi();
    pla[++N]=L;
    int l=1,r=L;
    while(l<=r)
    {
        int mid=(l+r)>>1,S=check(mid);
        if(S>M)r=mid-1;
        else l=mid+1;
    }
    printf("%d",r);
}

我们check的返回值就是最少需要搬走的石头个数,可以证明Ans就是最优值。

 

2.NOIP2012 借教室

因为答案具有单调性——答案之前的肯定都能借到。所以我们来二分答案。

那么该如何写check函数呢?

对于这道题,我们需要干的事有:区间增加和单点求值。可能大犇都想到了线段树,但这种复杂度贼高的算法最高只能过90分。我们没必要用高级算法,可以用一种小技巧处理——差分+前缀和。

所 谓差分,就是把区间的操作转移到对区间端点的操作。比如我们对一个都是0的数组,要在5~13这段区间加上3,只要在5这个点上加上3,在14这个点上减 掉3,这样在算前缀和的时候,0~4都是0,5~13都是3,14及以后又都成了0。这种操作修改是O(1),查询是O(n),但你可以一遍查询求出所有 的点是否合法。所以查询的总复杂度就是O(n+m),题目的复杂度就是O((n+m)logm)。

#include    <iostream>
#include    <cstdio>
#include    <cstdlib>
using namespace std;
int n,m,num[1010000],d[1010000],sta[1010000],end[1010000],Q[1010000],L,R;
int gi()
{
    int x=0;char ch=getchar();
    while(ch>9 || ch<0)ch=getchar();
    while(ch>/ && ch<:)x=(x<<1)+(x<<3)+ch-48,ch=getchar();
    return x;
}
bool check(int x)
{
    int total=0;
    for(int i=1;i<=n;++i)Q[i]=0;
    for(int i=1;i<=x;++i)Q[sta[i]]+=d[i],Q[end[i]+1]-=d[i];
    for(int i=1;i<=n;++i)
    {
        total+=Q[i];
        if(total>num[i])return false;
    }
    return true;
}
int main()
{
    n=gi();m=gi();
    for(int i=1;i<=n;++i)num[i]=gi();
    for(int i=1;i<=m;++i)d[i]=gi(),sta[i]=gi(),end[i]=gi();
    L=1;R=m;
    while(L<=R)
    {
        int mid=(L+R)>>1;
        if(check(mid))L=mid+1;
        else R=mid-1;
    }
    if(L>m)printf("0");
    else printf("-1\n%d",L);
    return 0;
}

我们check函数返回的是当前订单能否满足。

3.NOIP2011 聪明的质检员

一看就知道是二分答案:答案具有单调性,图像趋势类似于一个二次函数曲线,我们只要求出这个函数的顶点最近的整数就好了。二分答案,记录当前答案,比较一下就好了。

这里要解释一下那个式子的意思:L到R内所有满足Wj>W的j的个数乘以它们的体积和。这个可以用前缀和维护。开两个前缀和维护一下个数和体积和就好了。

#include    <cstdio>
#include    <cstdlib>
#include    <iostream>
#include    <algorithm>
#define LL long long int
LL n,m,s,l[201000],r[201000],V[201000],W[201000],Qnum[200100],QY[200100],maxR;
LL gi()
{
    LL x=0;char ch=getchar();
    while(ch>9 || ch<0)ch=getchar();
    while(ch>/ && ch<:)x=x*10+ch-48,ch=getchar();
    return x;
}
LL max(LL x,LL y){return x>y?x:y;}
LL min(LL x,LL y){return x<y?x:y;}
LL check(LL x)
{
    LL Ans=0;
    for(int i=1;i<=n;++i)
    {
        Qnum[i]=Qnum[i-1]+(W[i]>=x);
        QY[i]=QY[i-1]+V[i]*(W[i]>=x);
    }
    for(int i=1;i<=m;++i){Ans+=(Qnum[r[i]]-Qnum[l[i]-1])*(QY[r[i]]-QY[l[i]-1]);}
    return Ans-s;
}
int main()
{
    n=gi();m=gi();s=gi();
    for(int i=1;i<=n;++i)W[i]=gi(),V[i]=gi(),maxR=max(maxR,W[i]);
    for(int i=1;i<=m;++i)l[i]=gi(),r[i]=gi();
    {
        LL L=1,R=maxR,K=10000000000000;
        while(L<=R)
        {
            LL mid=(L+R)>>1,S=check(mid);
            if(S<0)R=mid-1,K=min(-S,K);
            else L=mid+1,K=min(S,K);
        }
        printf("%lld\n",K);
    }
    return 0;
}

 

check函数返回的是当前答案下的W的原值(不能带abs())

如果小于0,证明这个点在原点右边,要往左边挪。否则往右边挪。

 

那就这么草率的结尾吧!

二分答案入门乱讲