再也不用考虑边界的二分模板+二分算法应用题型归纳


提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

在解决大量的算法题之后,我(不是大佬)发现二分真的是一个很优秀的算法,应用的场景也非常之广,因为二分的时间复杂度只有(nlogn),应用二分算法能有效的降低优化代码的时间复杂度

一、二分模板

1.1模板

几乎不会出问题的二分模板,也是我从别的大佬那里学到的,
原博文详细解答
妈妈再也不用担心我的二分死循环啦~
no bb 上模板
假设数组下标从0~N-1;

	int l=-1,r=N;//l为最小值减1,r为最大值加1
	while(l+1!=r)
	{
		int mid=l+r>>1;
		if(check(mid)) l=mid;
		else r=mid;
	}

来说说为何不会越界
L最小值为-1,R最小值只能取到1,因为L+1!=R为循环结束条件,R最大值为N,同理则L的最大值为N-2,则(L+R)/2的取值范围是 [0,N)
mid的值始终位于0到N-1的闭区间,不会发生越界的错误;

1.2代码理解重点

由上述代码
最后l 为满足条件的点,r为刚好不满足那个点

二、应用场景

决策过程或序列是否满足局部单调性或局部舍弃性
一下是几个经典例题

2.1 单调性

y言之,有序的就可以用二分,无序的不一定用不了二分
其中最典型的就是最大值最小化,和最小值最大化。
这就要靠题目理解了确定是两种中哪类
常规思路只能通过暴力枚举的做法找出所有可能性,数据一大就会tle
但是通过二分的思想,猜测它去验证,再一次次缩小一半的范围得到最优解答案,时间复杂度直接降到O(nlog n)。
上例题

2.1.1最小值尽量大

洛谷P18224

题目描述

Farmer John 建造了一个有 N N N 2 ≤ N ≤ 1 0 5 2 \leq N \leq 10 ^ 5 2N105) 个隔间的牛棚,这些隔间分布在一条直线上,坐标是 x 1 , x 2 , ⋯   , x N x _ 1, x _ 2, \cdots, x _ N x1,x2,,xN 0 ≤ x i ≤ 1 0 9 0 \leq x _ i \leq 10 ^ 9 0xi109)。

他的 C C C 2 ≤ C ≤ N 2 \leq C \leq N 2CN)头牛不满于隔间的位置分布,它们为牛棚里其他的牛的存在而愤怒。为了防止牛之间的互相打斗,Farmer John 想把这些牛安置在指定的隔间,所有牛中相邻两头的最近距离越大越好。那么,这个最大的最近距离是多少呢?

输入格式

1 1 1 行:两个用空格隔开的数字 N N N C C C

2 ∼ N + 1 2 \sim N+1 2N+1 行:每行一个整数,表示每个隔间的坐标。

输出格式

输出只有一行,即相邻两头牛最大的最近距离。

#样例 #1

样例输入 #1

5 3
1
2
8
4
9

样例输出 #1

3
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1e5+10;
typedef long long ll;
ll a[N];
int n,c;

bool check(ll mid)
{	
	int i=0,j=1;
	int cnt=0;
	while(j<n&&cnt<=c){
		if(a[j]-a[i]>=mid){
			cnt++;
			i=j,j++; 
		}
		else j++;
	}
	return cnt>=c-1;
}

int main(){
	cin>>n>>c;
	for(int i=0;i<n;i++)
		cin>>a[i];
	sort(a,a+n);
	ll l=-1,r=a[n-1]+1,mid=0,ans=0;
	while(l+1!=r){
		mid=(l+r)/2;
		if(check(mid)) {
			l=mid;	//若符合继续增大数值
		}
		else r=mid;//不符合说明在另一边
	}
	printf("%d",l);
	return 0;
}

学废了吗,学废了来尝试一下这道难度进阶一点的题

洛谷P2678

题目描述

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

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

输入格式

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

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

输出格式

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

样例 #1

样例输入 #1

25 5 2 
2
11
14
17 
21

样例输出 #1

4

提示

输入输出样例 1 说明

将与起点距离为 2 2 2 14 14 14 的两个岩石移走后,最短的跳跃距离为 4 4 4(从与起点距离 17 17 17 的岩石跳到距离 21 21 21 的岩石,或者从距离 21 21 21 的岩石跳到终点)。

对于 100 % 100\% 100%的数据, 0 ≤ M ≤ N ≤ 50000 , 1 ≤ L ≤ 1 0 9 0 \le M \le N \le 50000,1 \le L \le 10^9 0MN50000,1L109

题目讲解

首先要靠我们的语文理解判断这是一个最小值最大化的问题,因为拿掉石子个数会有多种情况,但是每个情况最小值不同,然后确定最大的那个最小值。要是用暴力枚举出所有情况,会超时,所以我们先猜再验证
坑点在首位两个石头不能移,但是到首位的距离不能忽视

#include<iostream>
#include<cstring>
using namespace std;
const int N=5e4+10;
int a[N];
int L,n,m;

bool check(int mid)
{
	int cnt=m;
	for(int l=0,r=1;r<=n+1;r++){  //双指针
		while(a[r]-a[l]<mid) 
		{
			r++,cnt--;			
			if(cnt<0) return false;//能撤的撤完了,直接结束	
			if(r>n+1) return true;	//要判断不如数组越界,与尾石头距离超出的时候,撤的是它前面的石头
		}	
		l=r;
	}
	return true;
}

int main()
{
	cin>>L>>n>>m;
	for(int i=1;i<=n;i++)
		cin>>a[i];
	a[n+1]=L;      //尾石头也要存
	int l=0,r=L+1;
	while(l!=r-1)
	{
		int mid=l+r>>1;
		if(check(mid)) l=mid;
		else r=mid;
	}
	cout<<l;
	return 0;
}

2.2局部舍弃性

洛谷P1083

#[NOIP2012 提高组] 借教室
题目描述

在大学期间,经常需要租借教室。大到院系举办活动,小到学习小组自习讨论,都需要向学校申请借教室。教室的大小功能不同,借教室人的身份不同,借教室的手续也不一样。

面对海量租借教室的信息,我们自然希望编程解决这个问题。

我们需要处理接下来 n n n 天的借教室信息,其中第 i i i 天学校有 r i r_i ri 个教室可供租借。共有 m m m 份订单,每份订单用三个正整数描述,分别为 d j , s j , t j d_j,s_j,t_j dj,sj,tj,表示某租借者需要从第 s j s_j sj 天到第 t j t_j tj 天租借教室(包括第 s j s_j sj 天和第 t j t_j tj 天),每天需要租借 d j d_j dj 个教室。

我们假定,租借者对教室的大小、地点没有要求。即对于每份订单,我们只需要每天提供 d j d_j dj 个教室,而它们具体是哪些教室,每天是否是相同的教室则不用考虑。

借教室的原则是先到先得,也就是说我们要按照订单的先后顺序依次为每份订单分配教室。如果在分配的过程中遇到一份订单无法完全满足,则需要停止教室的分配,通知当前申请人修改订单。这里的无法满足指从第 s j s_j sj 天到第 t j t_j tj 天中有至少一天剩余的教室数量不足 d j d_j dj 个。

现在我们需要知道,是否会有订单无法完全满足。如果有,需要通知哪一个申请人修改订单。

输入格式

第一行包含两个正整数 n , m n,m n,m,表示天数和订单的数量。

第二行包含 n n n 个正整数,其中第 i i i 个数为 r i r_i ri,表示第 i i i 天可用于租借的教室数量。

接下来有 m m m 行,每行包含三个正整数 d j , s j , t j d_j,s_j,t_j dj,sj,tj,表示租借的数量,租借开始、结束分别在第几天。

每行相邻的两个数之间均用一个空格隔开。天数与订单均用从 1 1 1 开始的整数编号。

输出格式

如果所有订单均可满足,则输出只有一行,包含一个整数 0 0 0。否则(订单无法完全满足)

输出两行,第一行输出一个负整数 − 1 -1 1,第二行输出需要修改订单的申请人编号。

样例 #1
样例输入 #1

4 3 
2 5 4 3 
2 1 3 
3 2 4 
4 2 4

样例输出 #1

-1 
2

提示

【输入输出样例说明】

第 $1 $份订单满足后,$4 $天剩余的教室数分别为 0 , 3 , 2 , 3 0,3,2,3 0,3,2,3。第 2 2 2 份订单要求第 $2 $天到第 4 4 4 天每天提供$ 3 $个教室,而第 3 3 3 天剩余的教室数为$ 2$,因此无法满足。分配停止,通知第 2 2 2 个申请人修改订单。

【数据范围】
对于 100%的数据,有 1 ≤ n , m ≤ 1 0 6 , 0 ≤ r i , d j ≤ 1 0 9 , 1 ≤ s j ≤ t j ≤ n 1 ≤ n,m ≤ 10^6,0 ≤ r_i,d_j≤ 10^9,1 ≤ s_j≤ t_j≤ n 1n,m106,0ri,dj109,1sjtjn

题目讲解

这个题用暴力可以做出来,但是只能过50%的样例O(n^2)。打开思路,第一天开始减效率好像不是很高,如果第i天满足,那么i天之前的就不用判断了。这就是局部舍弃性。那怎样判断第i天是否满足呢,因为每次操作都是一个个区间,时间复杂度很高,但是差分能让复杂度降到O(1),最后的复杂的相加为O(N)。
接下来是代码

#include<iostream>
#include<cstring>
using namespace std;
const int N=1e6+10;
int rest[N],s[N],t[N];
long long add[N],d[N],need[N];//记得开long long!!!
int n,m;
int check(int mid)
{
	memset(add,0,sizeof add);
	for(int i=1;i<=mid;i++)//差分
	{
		add[s[i]]+=d[i];
		add[t[i]+1]-=d[i];
	}
	for(int i=1;i<=n;i++)
	{
		need[i]=need[i-1]+add[i];//需要的
		if(rest[i]<need[i]) 
		{
			return 0;
		} 
	}
	return true; 
}
int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		cin>>rest[i];
	for(int i=1;i<=m;i++)
	{
		cin>>d[i]>>s[i]>>t[i];
	}
	int l=0,r=m+1,mid;
	while(l+1!=r)
	{
		mid=l+r>>1; 
		if(check(mid)) l=mid;
		else r=mid;
	}
	
	if(l==m) cout<<"0";//注意是l
	else cout<<"-1"<<endl<<r;
	return 0;
}

此题还有个需要注意的点!!!我改了好久才找到。最后判断的时候,l左端点是记录符合情况的,也就是说最后是l到了最后的m天,不是r!!

好啦,二分就先讲到这里,二分是一个非常基础的算法,起到优化的作用,所以题目不会单纯的考二分,通常是与其他算法结合考察,之后二分的题目会记录在合集中。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值