从摆石头和牛舍问题看二分
题目背景
一年一度的“跳石头”比赛又要开始了!
题目描述
这项比赛将在一条笔直的河道中进行,河道中分布着一些巨大岩石。组委会已经选择好了两块岩石作为比赛起点和终点。在起点和终点之间,有 NNN 块岩石(不含起点和终点的岩石)。在比赛过程中,选手们将从起点出发,每一步跳向相邻的岩石,直至到达终点。
为了提高比赛难度,组委会计划移走一些岩石,使得选手们在比赛过程中的最短跳跃距离尽可能长。由于预算限制,组委会至多从起点和终点之间移走 MMM 块岩石(不能移走起点和终点的岩石)。
输入输出格式
输入格式:
第一行包含三个整数 L,N,ML,N,ML,N,M,分别表示起点到终点的距离,起点和终点之间的岩石数,以及组委会至多移走的岩石数。保证 L≥1L \geq 1L≥1 且 N≥M≥0N \geq M \geq 0N≥M≥0。
接下来 NNN 行,每行一个整数,第 iii 行的整数 Di(0<Di<L)D_i( 0 < D_i < L)Di(0<Di<L), 表示第 iii 块岩石与起点的距离。这些岩石按与起点距离从小到大的顺序给出,且不会有两个岩石出现在同一个位置。
输出格式:
一个整数,即最短跳跃距离的最大值。
输入输出样例
说明
输入输出样例 1 说明:将与起点距离为 222和 141414 的两个岩石移走后,最短的跳跃距离为 444(从与起点距离 171717 的岩石跳到距离 212121 的岩石,或者从距离 212121 的岩石跳到终点)。
另:对于 20%20\%20%的数据,0≤M≤N≤100 ≤ M ≤ N ≤ 100≤M≤N≤10。
对于50%50\%50%的数据,0≤M≤N≤1000 ≤ M ≤ N ≤ 1000≤M≤N≤100。
对于 100%100\%100%的数据,0≤M≤N≤50,000,1≤L≤1,000,000,0000 ≤ M ≤ N ≤ 50,000,1 ≤ L ≤ 1,000,000,0000≤M≤N≤50,000,1≤L≤1,000,000,000
附上0分代码
#include <bits/stdc++.h>
#define for(i,l,r) for(int i=l;i<=r;i++)
#define inf 0x7f7f
using namespace std;
long long ll,n,m,a[inf],mid,ans,l,r;
//首先是可行,其次考虑最优
bool check(int x){
long stone=0;
long long last;
for(i,1,n+1){
if(a[i]-a[last]<x) stone++;
else
{
last=i;
}
}
return stone<=m?1:0;
}
int main(){
cin>>ll>>n>>m;
for(i,1,n){
cin>>a[i];
}
a[n+1]=ll;
l=1,r=ll;
while(l<=r){
mid=(l+r)/2;
if(check(mid)) {
l=mid+1;
ans=mid;
}
else r=mid-1;
}
cout<<ans;
return 0;
}
附上满分代码
#include <bits/stdc++.h>
#define for(i,l,r) for(int i=l;i<=r;i++)
#define inf 0x7f7f7f
using namespace std;
long long ll,n,m,a[inf],mid,ans,l,r;
//首先是可行,其次考虑最优
bool check(int x){
long stone=0;
long long last=0;
for(i,1,n+1){
if(a[i]-a[last]<x) stone++;
else
{
last=i;
}
}
return stone<=m?1:0;
}
int main(){
cin>>ll>>n>>m;
for(i,1,n){
cin>>a[i];
}
a[n+1]=ll;
l=1,r=ll;
while(l<=r){
mid=(l+r)/2;
if(check(mid)) {
l=mid+1;
ans=mid;
}
else r=mid-1;//这些可以根据自己理解写
}
cout<<ans;
return 0;
}
看看有什么不同
是不是很坑
好了回归主题
二分就是一个从可行性问题到最优性问题
关键是看她可行解是在什么范围,可行解向最优解过度是左移还是右移
二分答案应该是在一个单调闭区间上进行的。也就是说,二分答案最后得到的答案应该是一个确定值,而不是像搜索那样会出现多解。二分一般用来解决最优解问题。刚才我们说单调性,那么这个单调性应该体现在哪里呢?
可以这样想,在一个区间上,有很多数,这些数可能是我们这些问题的解,换句话说,这里有很多不合法的解,也有很多合法的解。我们只考虑合法解,并称之为可行解。考虑所有可行解,我们肯定是要从这些可行解中找到一个最好的作为我们的答案, 这个答案我们称之为最优解。
最优解一定可行,但可行解不一定最优。我们假设整个序列具有单调性,且一个数x为可行解,那么一般的,所有的x'(x'<x)都是可行解。并且,如果有一个数y是非法解,那么一般的,所有的y'(y'>y)都是非法解。
那么什么时候适用二分答案呢?注意到题面:使得选手们在比赛过程中的最短跳跃距离尽可能长。如果题目规定了有“最大值最小”或者“最小值最大”的东西,那么这个东西应该就满足二分答案的有界性(显然)和单调性(能看出来)。
那就好办了。我们二分跳跃距离,然后把这个跳跃距离“认为”是最短的跳跃距离,然后去以这个距离为标准移石头。使用一个judge判断这个解是不是可行解。如果这个解是可行解,那么有可能会有比这更优的解,那么我们就去它的右边二分。为什么去右边?答案是,这个区间是递增的 ,而我们求的是最短跳跃距离的最大值,显然再右边的值肯定比左边大,那么我们就有可能找到比这更优的解,直到找不到,那么最后找到的解就有理由认为是区间内最优解。反过来,如果二分到的这个解是一个非法解,我们就不可能再去右边找了。因为性质,右边的解一定全都是非法解。那么我们就应该去左边找解。整个过程看起来很像递归,实际上,这个过程可以递归写, 也可以写成非递归形式,我个人比较喜欢使用非递归形式。
下一个问题,这个judge怎么实现呢?judge函数每个题有每个题的写法,但大体上的思想应该都是一样的——想办法检测这个解是不是合法。拿这个题来说,我们去判断如果以这个距离为最短跳跃距离需要移走多少块石头,先不必考虑限制移走多少块,等全部拿完再把拿走的数量和限制进行比对,如果超出限制,那么这就是一个非法解,反之就是一个合法解,很好理解吧。
可以去模拟这个跳石头的过程。开始你在i(i=0)位置,我在跳下一步的时候去判断我这个当前跳跃的距离,如果这个跳跃距离比二分出来的mid小,那这就是一个不合法的石头,应该移走。为什么?我们二分的是最短跳跃距离,已经是最短了,如果跳跃距离比最短更短岂不是显然不合法,是这样的吧。移走之后要怎么做?先把计数器加上1,再考虑向前跳啊。去看移走之后的下一块石头,再次判断跳过去的距离,如果这次的跳跃距离比最短的长,那么这样跳是完全可以的,我们就跳过去,继续判断,如果跳过去的距离不合法就再拿走,这样不断进行这个操作,直到i = n+1,为啥是n+1?河中间有n块石头,显然终点在n+1处。(这里千万要注意不要把n认为是终点,实际上从n还要跳一步才能到终点)。
模拟完这个过程,我们查看计数器的值,这个值代表的含义是我们以mid作为答案需要移走的石头数量,然后判断这个数量 是不是超了就行。如果超了就返回false,不超就返回true。//以上皆为转载
牛舍
#include <bits/stdc++.h>
#define for(i,l,r) for(int i=l;i<=r;++i)
using namespace std;
int n,m,a[101010],r,l,mid;
bool check(int x){
int cow=1;
int rgt=a[1]+x;
for(i,2,n) {
if(a[i]<=rgt) continue;
else
rgt=a[i]+x;
++cow;
}
return cow==m?1:0;
}
int main(){
cin>>n>>m;
for(i,1,n){
cin>>a[i];
}
sort(a+1,a+n+1);
l=0,r=a[n]-a[1];
do{
mid=(r+l)>>1;
if(check(mid)) {
l=mid+1;
}
else
r=mid-1;
}while(l<=r);
cout<<mid;
return 0;
}
P1316 丢瓶盖
陶陶是个贪玩的孩子,他在地上丢了A个瓶盖,为了简化问题,我们可以当作这A个瓶盖丢在一条直线上,现在他想从这些瓶盖里找出B个,使得距离最近的2个距离最大,他想知道,最大可以到多少呢?
输入输出格式
输入格式:
第一行,两个整数,A,B。(B<=A<=100000)
第二行,A个整数,分别为这A个瓶盖坐标。
输出格式:
仅一个整数,为所求答案。
输入输出样例
说明
限时3秒
0分代码
#include <bits/stdc++.h>
#define for(i,l,r) for(int i=l;i<=r;++i)
using namespace std;
long long n,m,a[101010],r,l,mid,ans,ct=1,now=1;
bool check(int x){
ct=1;
now=1;
for(i,2,n) {
if(a[i]-a[now]>=x)
++ct;
now=i;
}
return ct>=m?1:0;
}
int main(){
cin>>n>>m;
for(i,1,n){
cin>>a[i];
}
sort(a+1,a+n+1);
l=0,r=a[n]-a[1];
while(l<=r){
mid=(r+l)/2;
if(check(mid)) {
ans=max(ans,mid);
l=mid+1;
}
else
r=mid-1;
}
cout<<ans;
return 0;
}
100分代码
#include <bits/stdc++.h>
#define for(i,l,r) for(int i=l;i<=r;++i)
using namespace std;
long long n,m,a[101010],r,l,mid,ans,ct=1,now=1;
bool check(int x){
ct=1;
now=1;
for(i,2,n) {
if(a[i]-a[now]>=x){
++ct;
now=i;
}
}
return ct>=m?1:0;
}
int main(){
cin>>n>>m;
for(i,1,n){
cin>>a[i];
}
sort(a+1,a+n+1);
l=0,r=a[n]-a[1];
while(l<=r){
mid=(r+l)/2;
if(check(mid)) {
ans=max(ans,mid);
l=mid+1;
}
else
r=mid-1;
}
cout<<ans;
return 0;
}
真的就是一个括号让我调了半个小时啊啊,差点就放弃了