首先:
二分查找与二分答案有何区别?
二分查找:在一个已知的有序数据集上进行二分地查找
二分答案:答案有一个区间,在这个区间中二分,直到找到最优答案
什么是二分答案?
答案属于一个区间,当这个区间很大时,暴力超时。但重要的是——这个区间是对题目中的某个量有单调性的,此时,我们就会二分答案。每一次二分会做一次判断,看是否对应的那个量达到了需要的大小。
判断:根据题意写个check函数,如果满足check,就放弃右半区间(或左半区间),如果不满足,就放弃左半区间(或右半区间)。一直往复,直至到最终的答案。
其实,上面二分查找的例4,寻找的那个区间就是答案区间。
这不就相当于高中做选择题的时候,完了,不会做,那咋搞,把四个选项代进去看看对不对吧!哪个行得通那个就是答案!!
只不过我们现在要找的是最大的或者最小的答案。
如何判断一个题是不是用二分答案做的呢?
1、答案在一个区间内(一般情况下,区间会很大,暴力超时)
2、直接搜索不好搜,但是容易判断一个答案可行不可行
3、该区间对题目具有单调性,即:在区间中的值越大或越小,题目中的某个量对应增加或减少。
此外,可能还会有一个典型的特征:求...最大值的最小 、 求...最小值的最大。
1、求...最大值的最小,我们二分答案(即二分最大值)的时候,判断条件满足后,尽量让答案往前来(即:让r=mid),对应模板1;
2、同样,求...最小值的最大时,我们二分答案(即二分最小值)的时候,判断条件满足后,尽量让答案往后走(即:让l=mid),对应模板2;
先看一个经典的二分答案入门:
例1——木材加工
分析:看,答案就在区间(1,100000000)里,就等着我们找呢,暴力肯定超时,那可能就用二分。
满足条件:
1,答案在一个区间里。
2,如果给一个答案,给目标一个小段的长度,很容易判断是否到K个了。
3,具有单调性,目标小段越长,那能切出的段数越少,目标小段越短,能切出的段数越多。而最终需要K个,从而很容易判断一个答案行不行。
一看求啥,求最长长度,最长?这不,关门打狗,模板2! !
那,判断条件?模板2,如果满足判断,l=mid。啥叫满足呢?那肯定是满足需要的段数了呗!
#include<iostream>
using namespace std;
const int N=1e5+10;
long long a[N],n,m,sum,maxa;
int check(int mid)
{
int sum=0;
for(int i=1;i<=n;i++){
sum+=a[i]/mid;
}
if(sum>=m) return 1; //总段数大于等于所需要的
return 0;
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i],sum+=a[i];
if(a[i]>maxa) maxa=a[i];
}
if(sum<m){cout<<0;return 0;} //先判断是否有解
int l=1,r=maxa;
while(l<r) //模板2
{
int mid=l+r+1>>1;
if(check(mid)) l=mid;
else r=mid-1;
}
cout<<l;
return 0;
}
是不是感觉很有意思?
再来看个经典的
例2——跳石头
分析:看题,这是啥?最短距离的最大值!这不就是二分答案的典型特征?还想啥,二分!
求最大?上模板2!! 那,判断条件?
这时候就要注意了,我们二分的是最短距离,通过二分将这个最短距离(答案)最大化。那我们判断的时候肯定要保证mid是最短距离。
如何保证?我们要求抽过石头剩下的石头中,两个石头间的最短距离为mid,那就要保证剩下的任意两个间距都要大于等于mid。要保证这个,那就只能挑间距大于等于mid的石头跳,中间的石头都将会被抽走。
最后,计数可以被抽走的石头。如果可以被抽走的石头个数小于等于需要抽的M个了,就说明满足条件。因为:既然抽了小于M个都能满足剩下的石头中,两石头间的距离都大于等于mid了,那抽M个,更能满足!
有点晕?没关系!看了代码就懂了!
#include<iostream>
using namespace std;
const int N=50010;
int a[N],n,len,m,mina=1e9+1,b[N];
int check(int mid) //检查,是否最短距离为mid,如果两石头间距小于mid,不满足,移走
{
int cnt=0;
int i=0,now=0; //i表示目标位置,now为当前位置。
while(i<n+1){
i++;
if(a[i]-a[now]<mid){ //两石头间距离小于mid,mid不是最短距离,不满足,移走该石头
cnt++;
}
else{ //符合,跳过去
now=i;
}
}
if(cnt<=m) return 1; //移走的石头个数小于 M,就能保证了任意两剩下的石头间距大于等于最短距离mid,那移走M个,更能保证
return 0;
}
int main(){
cin>>len>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
if(a[i]<mina) mina=a[i];
}
a[0]=0,a[n+1]=len; //首尾都有石头
if(n==0){ //特判掉起点和终点之间没有石头的情况,可以想一下为什么。评论区中有答案。感谢 luojias 同学的hack数据!
cout<<len; return 0;
}
//二分答案:检查每一个答案(最短距离mid)是否符合要求
long long l=1,r=1e10;
while(l<r) //模板2
{
int mid=l+r+1>>1;
if(check(mid)) l=mid; //要的是距离的最大,所以尽可能地往右走
else r=mid-1;
}
cout<<l;
return 0;
}
还没懂?没关系,我们再看一题!
例3——丢瓶盖
分析:距离最近的2个瓶盖距离最大? 最短距离的最大值! 二分!!
看——求最大值,模板二!
判断条件check:与上题不同的是,这题是保证拿走的那些瓶盖之间的最短距离最大(上题是保证剩下的石头最短距离最大,这两个容易混淆。是我没错了… ),那么,遍历的时候,只要满足这次和上次拿的那个瓶盖间距大于等于mid,就可以拿了。这样就保证了我们找的最短距离mid是最短的间距。
最后如果拿出的总瓶盖数大于等于目标值,就说明满足判断。因为:既然拿了超过目标值就能满足拿走的瓶盖间距大于等于mid,那拿目标值(B)个,肯定更能满足!
#include<iostream>
#include<algorithm>
using namespace std;
const int N=100010;
int a[N],n,m,maxa;
//注意:这是拿出来的那些里,mid为最短距离,和跳石头不同的是,跳石头是在留下的里面,mid为最短距离
int check(int mid)
{
//now为最后一次拿的瓶盖位置,i为当前遍历的位置
int i=1,now=1,cnt=0; 注意:第一个瓶盖必选,才能保证剩下的距离最大,从而挑出的瓶盖间最短距离最大化
while(i<n)
{
i++;
if(a[i]-a[now]>=mid){ //保证拿走的瓶盖间距大于等于mid,才拿这个瓶盖,否则不能保证mid为最短距离
now=i,cnt++;
}
}
if(cnt+1>=m) return 1; //如果拿出的总个数大于等于m,都能保证拿走的瓶盖间距大于等于mid,那拿出来m个,肯定也能满足!!
return 0;
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
if(a[i]>maxa) maxa=a[i];
}
sort(a+1,a+n+1);
int l=0,r=maxa;
while(l<r) //模板2
{
int mid=l+r+1>>1;
if(check(mid)) l=mid;
else r=mid-1;
}
cout<<l<<endl;
}
做了上面两题,我们差不多又可以总结出规律了,心里是不是有点小激动?
最大值最小,最小值最大 类 问题解题方向:
最短距离最大化问题:保证任意区间距离要比最短距离mid大或相等(这样,mid才是最短距离)即:区间的距离>=mid
最长距离最小化问题:保证任意区间距离要比最大距离mid小或相等(这样,mid才是最大距离)即:区间的距离<=mid
哈哈哈,是不是太有趣啦?
快快,趁热打铁,再来!!
例4——数列分段 Section II
分析:没错,这次是最大值最小!
求最小值? 哎对,模板1!
判断条件:要保证:每一段的和都小于等于最大值。也就是说,只要这一段的和加上下一个值大于最大值了,那下一个值加不得,得分段!接着段数++;
最后,统计出的总段数(cnt+1)小于等于目标值了,那就算满足;因为,既然分了小于目标值个段都能保证每段的和小于等于最大值,那么分目标值个段肯定还能保证!
还有一个小细节:l,和 r 的初始化。
所有段中的最大和肯定大于等于数列中的最大值(因为最大值最少单成一段,那所有段中的最大的和肯定要大于等于最大值),所以l要初始化为maxa。
同样,所有段中和的最大值,最大不过数列中的所有值的和。
#include<iostream>
using namespace std;
const int N=100010;
typedef long long ll;
ll a[N],n,m,summ,mina=1e9+1,maxa;
int check(int mid)
{
ll cnt=0,sum=0;
for(int i=1;i<=n-1;i++)
{
sum+=a[i];
if(sum+a[i+1]>mid) cnt++,sum=0; //不能满足 "区间间距小于最大距离",那就分段
}
if(cnt+1<=m) return 1; //总的段数小于等于需要的段数,这样都能满足mid为每段的最大值,那么多分几段,肯定还能满足
return 0;
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i],summ+=a[i];
if(a[i]<mina) mina=a[i];
if(a[i]>maxa) maxa=a[i];
}
int l=maxa,r=summ; //l要设为maxa,所有段的最大值肯定大于等于maxa
while(l<r)
{
int mid=l+r>>1;
if(check(mid)) r=mid; //求的是最大值的最小,故尽量往左来
else l=mid+1;
}
cout<<l;
return 0;
}
好啦,至此,二分答案你就差不多掌握了。方法说的都是实打实的;
最后,在给出几道练习题吧:
1、进击的奶牛
2、路标设置
3、最佳牛围栏
4、kotori的设备
本文的课后练习题的答案在这个博客里。
学习链接:https://blog.csdn.net/Mr_dimple/article/details/114656142