zoukankan      html  css  js  c++  java
  • 二分答案入门乱讲

    1.关于二分答案

    //2017年12月有更新。

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

    对于一些问题,它的解满足单调性,即如果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,ans=1;
    while(l<=r)
    {
        int mid=(l+r)>>1;
        if(check(mid))l=mid+1,ans=mid;
        else r=mid-1;
    }
    printf("%d",ans);

    是啊,zz的板子谁都会。重点就在check函数上面。我们要使用时间复杂度优秀的check来写。怎么写出最好的方法呢?分析题目,优化算法,然后大胆猜想不用证明。多做题目,积累经验(这是最吼的!)。然后因为我走歪了,check一向非主流,但莫名好用啊(有些题目的check只要有不合法就可以立即弹出,复杂度可以降到玄学级)一般二分答案复杂度是稳定的log(r-l+1),总复杂度是O(log*f(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
    %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
    ",K);
        }
        return 0;
    }

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

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

    那就这么草率的结尾吧!

    
    
  • 相关阅读:
    第二十七天笔记
    hdoj 1024
    poj 2253
    超水的一道最短路poj2387
    打算要做的题目
    poj 3128 关于置换群的规律
    poj 1721
    poj 1026 置换的应用(小试牛刀)
    置换的一项运用 poj3270
    Codeforces Round #483 (Div. 2) D. XOR-pyramid dp的应用
  • 原文地址:https://www.cnblogs.com/fenghaoran/p/6391052.html
Copyright © 2011-2022 走看看