洛谷 P2678跳石头 & P3853 路标设置(二分)

P2678:

题目描述

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

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

输入格式

第一行包含三个整数 L,N,M,分别表示起点到终点的距离,起点和终点之间的岩石数,以及组委会至多移走的岩石数。保证 L≥1 且NM≥0。

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

输出格式

一个整数,即最短跳跃距离的最大值

分析:

先贴一手洛谷Shownzhou大佬的一段话:

“那么什么时候适用二分答案呢?注意到题面:使得选手们在比赛过程中的最短跳跃距离尽可能长。如果题目规定了有
“最大值最小”或者

最小值最大”的东西,那么这个东西应该就满足二分答案的有界性(显然)和单调性(能看出来)。

那就好办了。我们二分跳跃距离,然后把这个跳跃距离“认为”是最短的跳跃距离,然后去以这个距离为标准移石头。使用一个
judge判断这个解是不是可行解。如果这个解是可行解,那么有可能会有比这更优的解,那么我们就去它的右边二分。为什么去右边?答案是,这个区间是
递增的 ,而我们求的是最短跳跃距离的
最大值,显然再右边的值肯定比左边大,那么我们就有可能找到比这更优的解,直到找不到,那么最后找到的解就有理由认为是区间内最优解。反过来,如果二分到的这个解是一个非法解,我们就不可能再去右边找了。因为性质,右边的解一定全都是非法解。那么我们就应该去左边找解。整个过程看起来很像递归,实际上,这个过程可以递归写, 也可以写成非递归形式,我个人比较喜欢使用非递归形式。

下一个问题,这个judge怎么实现呢?judge函数每个题有每个题的写法,但大体上的思想应该都是一样的——
想办法检测这个解是不是合法。拿这个题来说,我们去判断如果以这个距离为最短跳跃距离需要移走多少块石头,先不必考虑限制移走多少块,等全部拿完再把拿走的数量和限制进行比对,如果超出限制,那么这就是一个非法解,反之就是一个合法解,很好理解吧。”

这几句话对初学者我而言,可以说是圣经,建议反复阅读。

二分答案,需要我们在答案的可能区间内找出真正的答案,而寻找的方法就是二分,所以要求这个区间是单调的,是有界的。judge函数的判断是二分答案的核心,难一点的题可能就在这里下手。对于这一题,答案区间很明显就是【1,L】,有界而且递增。

judge函数就很关键。在这里,我们可以把区间【1,L】中的某一个数x作为结果,然后进行模拟,看看在最短跳跃距离的最大值为x的情况下,需要移走的岩石数m是多少。若m<M,行得通,但是我们仍然需要看看>x的数能否满足;m>M,说明x太大了,应在x的左边寻找答案。

#include<iostream>
#include<algorithm>
typedef long long ll;
using namespace std;
ll n,m,l;
ll d[50005],f[50005];
bool judge(ll x)
{
    ll cnt=0,now=0,i=1;
    while(i<=n+1){
        if(d[i]-d[now]<x) cnt++;//移走 i
        //* 为什么还要考虑i==n+1 难道还能移走第n+1即终点的石头吗 
        else now=i;
        i++;
    }
    if(cnt>m) return false;
    else return true;
}
int main()
{
    cin>>l>>n>>m;
    for(ll i=1;i<=n;i++) cin>>d[i];
    d[n+1]=l;
    ll left=1,right=l;
    while(left<=right){
        ll mid=left+(right-left)/2;
        if(judge(mid)) left=mid+1;
        else right=mid-1;
    }
    cout<<right<<endl;
    return 0;
}

为什么还要考虑i==n+1 难道还能移走第n+1即终点的石头吗 ?

这个问题shownzhou大佬似乎也没有考虑到,为啥写i==n+1,而不是i==n,不是说不能移走终点即n+1号石头吗?其实,考虑i==n+1不再是为了移走i,而是要不要移走now,如果说d[n+1]-d[now]<x但是不能移动n+1,所以我们只能选择把我们现在踩着的位置的石头移除,cnt++。

这是讨论区的一位大佬贴的。%%%


P3853:

题目描述

现在政府决定在公路上增设一些路标,使得公路的“空旷指数”最小。他们请求你设计一个程序计算能达到的最小值是多少。请注意,公路的起点和终点保证已设有路标,公路的长度为整数,并且原有路标和新设路标都必须距起点整数个单位距离。

输入格式

第 1 行包括三个数 L,N,K,分别表示公路的长度,原有路标的数量,以及最多可增设的路标数量

第 2 行包括递增排列的 N 个整数,分别表示原有的 N 个路标的位置。路标的位置用距起点的距离表示,且一定位于区间 [0,L] 内。

输出格式

输出 1 行,包含一个整数,表示增设路标后能达到的最小“空旷指数”值

分析:

这个题其实和上一个题几乎一样,但是有好几个坑,我就每个坑都跳了一次

#include<iostream>
#include<algorithm>
typedef long long ll;
using namespace std;
ll L,n,k;
ll d[100002];
bool judge(ll x)
{
    ll cnt=0,now=1;
    for(int i=2;i<=n;i++){
        if(d[i]-d[now]>x) {
            cnt+=((d[i]-d[now])/x);
            if((d[i]-d[now])%x==0) cnt--;
        }
        now=i;
    }
    if(cnt<=k) return true;
    else return false;
}
int main()
{
    cin>>L>>n>>k;
    for(int i=1;i<=n;i++) cin>>d[i];
    ll l=1,r=L,ans=0;
    while(l<=r){
        ll mid=l+(r-l)/2;
        //二分答案 还是用ans写好
        if(judge(mid)) ans=mid,r=mid-1;
        else l=mid+1;
    }
    cout<<ans<<endl;
    return 0;
} 

坑就是“思想被上一个题固化了”。这个是插,上一个是拔,当d[i]-d[now]>x时,我们需要插(d[i]-d[now])/x根柱子,注意如果(d[i]-d[now])%x==0,应该插(d[i]-d[now])/x-1根。

虽然说,这两个题难度都不是很高(当初一直WA的时候可不是这样说的),但是极大提高了我对二分答案的理解。


再次膜拜:Shownzhou大佬