二分算法

二分入门

摆道例题:

给出一个上升的序列,每次询问区间 [ l , r ] [l,r] [l,r] 中大于等于 x x x 的数有多少个。

每次取当前区间的中点, m i d = l + r 2 mid=\dfrac {l+r}2 mid=2l+r,假如 m i d mid mid 位置上的数小于 x x x,那么显然 [ l , m i d ] [l,mid] [l,mid] 这个区间内的数都小于 x x x,但是我们还不知道 [ m i d + 1 , r ] [mid+1,r] [mid+1,r] 这个区间内的情况,于是将 l l l 变成 m i d + 1 mid+1 mid+1,对区间 [ m i d + 1 , r ] [mid+1,r] [mid+1,r] 继续二分。假如 m i d mid mid 位置上的数大于 x x x,那么就对区间 [ l , m i d ] [l,mid] [l,mid] 进行二分。

这样二分下去,最后 l l l 会等于 r r r,得到一个分界点,这个分界点的左边以及自己都小于等于 x x x,这个分界点的右边都大于 x x x,那么这个分界点到 r r r(这个 r r r 是一开始的 r r r)这一段的数的个数就是答案。

这是二分最简单也十分常见的应用:二分查找

为了方便理解,这里贴上代码:

//设a数组为那个序列,
l=ask_l,r=ask_r,ans;
while(l<=r)//假如还没找到分界点
{
	int mid=(l+r)/2;
	if(a[mid]==x){ans=mid;break;}
	if(a[mid]<x)l=mid+1;
	else r=mid-1;
}

从这道例题,我们可以分析出二分的几个特点

  1. 二分只能用来判断当前这是不是一个可行解,而不能用二分来求解。
    比如说这个例题,我们每次只能判断当前这个点是否大于 x x x,而不能求解。只有做到最后找到分界点了,才能计算答案。
  2. 进行二分的序列要满足单调性。
    单调性这个东西,简而言之,你在序列中选一个点,这个点左边的部分或者是它右边的部分的可行性一定和它自己的可行性相同。
    比如说上面的例题,对于任意一个点,假如它不可行(也就是小于等于 x x x),那么它左边的点都一定不可行,假如它可行(也就是大于 x x x),那么它右边的点一定都可行。
    单调性是进行二分的前提条件,有了单调性,我们每次判断当前区间的中点的时候,也就相当于判断完了当前区间的一半,这样每次判断一半,就是二分的优秀所在。所以二分的时间复杂度很显然了—— O ( l o g 2 ( n ) ) O(log_2(n)) O(log2(n))

对于大部分题目,其单调性没有上面这题写的这么赤裸裸,需要我们自己去发现。

二分进阶

还是先摆个例题:通往奥格瑞玛的道路

题目大意: 有一张图,从起点出发,有一个血量,经过一条边会损失一定的血量,每个点还有一个权值,现在需要找到一条路径,使得在可以走到终点,并且不死亡的前提下,让路径上的点权的最大值最小。输出这个点权。

这是二分的一个经典应用:二分答案

发现其实答案是有单调性的,于是可以对其进行二分。

具体一点:

假如答案为 a n s ans ans,那么假如我们将图中点权大于 a n s ans ans 的点删掉,对答案是没有影响的,因为这些点我们压根就不会走。那么显然,如果 a n s ans ans 越大,那么保留的点的数量就越多。那么就有了这样一个单调性:假如只保留点权小于等于 x x x 的点,用这些点可以走到终点的话,那么对于任意 y ( y > x ) y(y>x) y(y>x),假如只保留点权小于等于 y y y 的点,也一定是可以走到终点的。

于是可以二分这个边权,然后对于每个边权建一张图,跑一次SPFA即可判断。

代码如下:

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define maxn 10010

int n,m,hp;
int a[maxn];
struct node{int y,z,next;};
node e[10*maxn];
int first[maxn],len;
void buildroad(int x,int y,int z)
{
	e[++len]=(node){y,z,first[x]};
	first[x]=len;
}
struct edge{int x,y,z;};//Ç¿ÆÈÖ¢¶à¿ªÒ»ÖÖÀàÐÍqwq 
edge bian[5*maxn];
int q[maxn],st,ed;
int dis[maxn];
bool v[maxn];
void SPFA()
{
	st=1,ed=2;
	q[st]=n;
	v[n]=true;
	memset(dis,63,sizeof(dis));
	dis[n]=0;
	while(st!=ed)
	{
		int x=q[st];
		for(int i=first[x];i;i=e[i].next)
		{
			int y=e[i].y;
			if(dis[x]+e[i].z<dis[y])
			{
				dis[y]=dis[x]+e[i].z;
				if(!v[y])
				{
					v[y]=true;
					q[ed++]=y;
					if(ed>n)ed=1;
				}
			}
		}
		st++;
		if(st>n)st=1;
		v[x]=false;
	}
}
bool check(int x)
{
	len=0;
	memset(first,0,sizeof(first));//记得清空之前的边
	for(int i=1;i<=m;i++)
	if(a[bian[i].x]<=x&&a[bian[i].y]<=x)//假如这条边连接的两个点的权值都小于等于x就建这条边
	{
		buildroad(bian[i].x,bian[i].y,bian[i].z);
		buildroad(bian[i].y,bian[i].x,bian[i].z);
	}
	SPFA();//反向跑一次SPFA,看一下每个点到终点的最短路,也就是每个点到终点最少需要花费的血量
	return dis[1]<hp;//假如起点到终点需要花费的血量比歪嘴哦的血量小,那么就是符合要求的
}

int main()
{
	scanf("%d %d %d",&n,&m,&hp);
	int l=999999999,r=0; 
	for(int i=1;i<=n;i++)
	scanf("%d",&a[i]),l=min(l,a[i]),r=max(r,a[i]);//找到边权的最大值和最小值作为二分的l和r
	for(int i=1;i<=m;i++)
	scanf("%d %d %d",&bian[i].x,&bian[i].y,&bian[i].z);
	int ans=-1;
	while(l<=r)
	{
		int mid=l+r>>1;
		if(check(mid))ans=mid,r=mid-1;//记录下最小的满足要求的mid
		else l=mid+1;
	}
	if(ans!=-1)printf("%d",ans);
	else printf("AFK");
}

有人会问,二分加SPFA不会TLE吗?

事实上不会,因为二分的时间复杂度只有 O ( l o g n ) O(logn) O(logn),所以里面判断的时间复杂度在 O ( n ) O(n) O(n) ~ O ( n l o g n ) O(nlogn) O(nlogn)都差不多是可以接受的。

二分还有很多神奇的用法,留给大家自己探索。

摆一道进阶题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值