二分答案浅析

2020年7月22日 0 作者 小字、小瓶子

及 跳石头[NOIP 2015]、砍树(洛谷P1873)题解

之前为了实验室讲题做的PPT,结果没用上,那干脆改成教程好了。。。

ps:PPT在文末自取

二分答案,就是用二分的思想,通过对可能的答案区间进行折半查找,不断缩减范围,最终确定答案的方法。

大多数情况下用于求解满足某种条件下的最大(小)值,前提是答案具有单调性较明确的界限,同时也可以理解为是一种倒推方法(先找答案在判断答案是否可行、有没有更优解)。

答案的单调性大多数情况下可以转化为一个函数,其单调性证明多种多样,如下:

  • 移动石头的个数越多,答案越大(NOIP2015跳石头)。
  • 前i天的条件一定比前 i + 1 天条件更容易(NOIP2012借教室)。
  • 满足更少分配要求比满足更多的要求更容易(NOIP2010关押罪犯)。
  • 满足更大最大值比满足更小最大值的要求更容易(NOIP2015运输计划)。
  • 时间越长,越容易满足条件(NOIP2012疫情控制)。

可以解决的问题 :

  • 求最大的最小值(NOIP2015跳石头)。
  • 求最小的最大值(NOIP2010关押罪犯)。
  • 求满足条件下的最小(大)值。
  • 求最靠近一个值的值。
  • 求最小的能满足条件的代价

模板就是二分,面对整数时,这个模板是万能的,只需要根据情况更改r=mid-1和 l=mid+1的位置即可;

int l=min, r=max;//min是答案的最小值,max是答案的最大值    
while(l<=r) {
    int mid=(l + r)>>1,q=check(mid);//“>>1”相当于“/2”
    if(q>m)r=mid-1;
    else l=mid+1;
}

check函数则要视题目而定,其作用是帮助检验当前mid值是否符合要求。

砍树(洛谷P1873)

•伐木工人米尔科需要砍倒M米长的木材。这是一个对米尔科来说很容易的工作,因为他有一个漂亮的新伐木机,可以像野火一样砍倒森林。不过,米尔科只被允许砍倒单行树木。

•米尔科的伐木机工作过程如下:米尔科设置一个高度参数H(米),伐木机升起一个巨大的锯片到高度H,并锯掉所有的树比H高的部分(当然,树木不高于H米的部分保持不变)。米尔科就行到树木被锯下的部分。

•例如,如果一行树的高度分别为20,15,10和17,米尔科把锯片升到15米的高度,切割后树木剩下的高度将是15,15,10和15,而米尔科将从第1棵树得到5米,从第4棵树得到2米,共得到7米木材。

•米尔科非常关注生态保护,所以他不会砍掉过多的木材。这正是他为什么尽可能高地设定伐木机锯片的原因。帮助米尔科找到伐木机锯片的最大的整数高度H,使得他能得到木材至少为M米。换句话说,如果再升高1米,则他将得不到M米木材。

输入格式:

第1行:2个整数N和M,N表示树木的数量(1<=N<= 10^6  ),M表示需要的木材总长度(1<=M<=2* 10^9  )

第2行:N个整数表示每棵树的高度,值均不超过10^9  。所有木材长度之和大于M,因此必有解。

输出格式:

第1行:1个整数,表示砍树的最高高度。

这个题的答案有明显的单调性,砍树的高度越低,得到的木材就越多,所以我们可以直接从最高的树的高度往下开始枚举,直到找到答案。

但是如果直接枚举,这道题最大计算量达到10^15,直接枚举必然超时,所以我们就可以使用二分法节省时间。

二分答案解法:

#include<iostream>
#include<cstdio>
long long m,n,a[1000005],temp;
using namespace std;
long long check(long long x) {
    //check函数是二分的最重要的一环
    long long ans=0;
    for(int i=1;i<=n;i++) {
        if(a[i]>x)
        ans=ans-x+a[i];    
        //ans用来记录能够得到的木材长度
     } 
     return ans;
}
int main() {
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++) {
        scanf("%d",&a[i]);
        temp=max(temp,a[i]);    
        //temp用来记录最高的树的高度
    }
    long long l=1,r=temp;
    //把右边界设成最高的树的高度
    while(l<=r) {
        //二分操作
        long long mid=(l+r)>>1,q=check(mid);
        if(q<m)r=mid-1;
        else l=mid+1;
    }
    printf("%d",r);
    return 0;
}

check函数是二分中最重要的一环,二分的模板可以背,但是check函数就因题而异了。

跳石头[NOIP 2015]

•这项比赛将在一条笔直的河道中进行,河道中分布着一些巨大岩石。组委会已经选择好了两块岩石作为比赛起点和终点。在起点和终点之间,有 N 块岩石(不含起点和终点的岩石)。在比赛过程中,选手们将从起点出发,每一步跳向相邻的岩石,直至到达终点。

•为了提高比赛难度,组委会计划移走一些岩石,使得选手们在比赛过程中的最短跳跃距离尽可能长。由于预算限制,组委会至多从起点和终点之间移走 M 块岩石(不能移走起点和终点的岩石)。

【输入格式】

输入文件第一行包含三个整数 L,N,M,分别表示起点到终点的距离,起点和终 点之间的岩石数,以及组委会至多移走的岩石数。

接下来 N 行,每行一个整数,第 i 行的整数 Di(0 < Di < L)表示第 i 块岩石与 起点的距离。这些岩石按与起点距离从小到大的顺序给出,且不会有两个岩石出现在同 一个位置。

【输出格式】

输出文件只包含一个整数,即最短跳跃距离的最大值。

check函数的思路(贪心)

从起点出发,先选定一段距离mid(也就是搬走石头后最长的最短跳跃距离),若前面的石头B与你所站着的石头A的距离小于mid,就把B搬掉,记录一下;如果不,就把B留下,再跳到石头B上。

照这个步骤循环,每次循环中,如果搬掉的石头多了,说明mid太长,就把距离mid定小点;如果搬掉的石头少了,说明mid可能还能更大,就把mid定大点。

如此多次循环,直至二分结束,就可以得到答案。

程序:

#include<iostream>
#include<cstdio>
using namespace std;
int np[50005],L,m,n;
int check(int x) {
    int ans=0,t=0;
    for(int i=1;i<=n;i++) {
        while(np[i]-t<x&&i<=n) {
            ans++;i++;
        }
        t=np[i];
    }
    return ans;
}
int main() {
    cin>>L>>n>>m;
    for(int i=1;i<=n;i++)
        scanf("%d",&np[i]);
    np[n+1]=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);
    return 0;
}

注意:不要忘记将起点和终点放入存放石头位置的数组(当然终点并不能被搬走)。

其他利用二分答案的题目

  • [NOIP 2010] 关押罪犯
  • [NOIP 2011] 聪明的质监员
  • [NOIP 2012] 借教室
  • [NOIP 2012] 疫情控制
  • [NOIP 2018] 赛道修建