提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
在解决大量的算法题之后,我(不是大佬)发现二分真的是一个很优秀的算法,应用的场景也非常之广,因为二分的时间复杂度只有(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 2≤N≤105) 个隔间的牛棚,这些隔间分布在一条直线上,坐标是 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 0≤xi≤109)。
他的 C C C( 2 ≤ C ≤ N 2 \leq C \leq N 2≤C≤N)头牛不满于隔间的位置分布,它们为牛棚里其他的牛的存在而愤怒。为了防止牛之间的互相打斗,Farmer John 想把这些牛安置在指定的隔间,所有牛中相邻两头的最近距离越大越好。那么,这个最大的最近距离是多少呢?
输入格式
第 1 1 1 行:两个用空格隔开的数字 N N N 和 C C C。
第 2 ∼ N + 1 2 \sim N+1 2∼N+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 L≥1 且 N ≥ M ≥ 0 N \geq M \geq 0 N≥M≥0。
接下来 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 0≤M≤N≤50000,1≤L≤109。
题目讲解
首先要靠我们的语文理解判断这是一个最小值最大化的问题,因为拿掉石子个数会有多种情况,但是每个情况最小值不同,然后确定最大的那个最小值。要是用暴力枚举出所有情况,会超时,所以我们先猜再验证。
坑点在首位两个石头不能移,但是到首位的距离不能忽视
#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
1≤n,m≤106,0≤ri,dj≤109,1≤sj≤tj≤n。
题目讲解
这个题用暴力可以做出来,但是只能过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!!
好啦,二分就先讲到这里,二分是一个非常基础的算法,起到优化的作用,所以题目不会单纯的考二分,通常是与其他算法结合考察,之后二分的题目会记录在合集中。