算法竞赛icpc之二分(3)

首先:

二分查找与二分答案有何区别?
二分查找:在一个已知的有序数据集上进行二分地查找
二分答案:答案有一个区间,在这个区间中二分,直到找到最优答案

什么是二分答案?
答案属于一个区间,当这个区间很大时,暴力超时。但重要的是——这个区间是对题目中的某个量有单调性的,此时,我们就会二分答案。每一次二分会做一次判断,看是否对应的那个量达到了需要的大小。
判断:根据题意写个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

  • 20
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值