浅谈二分

二分算法(未完)

前言

二分属于分治的一种,很多题都需要用到这种高效简洁的算法

所以,二分是必掌握的算法,这篇博客就是我的二分的学习记录qwq


目录

  • 二分算法知识
  1. 整数集合上的二分
  2. 实数域上的二分法
  3. 二分法的常见模型
  • 二分答案题目

  • 二分答案题解

  • 二分查找题目

  • 二分查找题解


二分

PS:以下部分内容摘抄自李煜东的《算法竞赛进阶指南》

二分的基础用法就是在单调序列或单调函数中进行查找。因此当问题的答案具有单调性时,就可以通过二分把求解转化为判定。进一步地,我们还可以扩展到通过三分法去解决单峰函数的极值以及相关问题

  • 整数集合上的二分

在单调递增序列a中查找≥x的数中最小的一个(即x或x的后继):

while(l<r) {
    int mid=(l+r)>>1;  //(l+r)/2
    if(a[mid]>=x) r=mid;
    else l=mid+1;
}

a[mid]≥x,根据序列a的单调性,mid之后的数会更大,所以≥x的最小的数不可能在mid之后,可行区间应该缩小为左半段。因为mid也可能是答案,故此时应取r=mid。同理,若a[mid]<x,则取l=mid-1

在单调递增序列a中查找≤x的数中最大的一个(即x或x的前驱):

while(l<r) {
	int mid=(l+r+1)>>1;
	if(a[mid]<=x) l=mid;
	else r=mid-1;
}

a[mid]≤x,根据序列a的单调性,mid之前的数会更小,所以≤x的最大的数不可能在mid之前,可行区间应该缩小为右半段。因为mid也可能是答案,故此时应取l=mid。同理,若a[mid]>x,则取r=mid-1

如上所示,二分写法可能会有两种形式:

  1. 缩小范围时,r=mid,l=mid+1,取中间值时,mid=(l+r)>>1
  2. 缩小范围时,l=mid,r=mid-1,取中间值时,mid=(l+r+1)>>1

为什么要区分mid的取值形式?

因为假如第二段代码也采用mid=(l+r)>>1,那么就会造成死循环

所以配套的mid取法时必要的(当然,二分还有其他写法,这里不多赘述,后面的练习题目会有涉及)

补充:STL中的lower_bound和upper_bound函数实现了在一个序列中二分查找某个整数x的后继

  • 实数域上的二分

实数域上的二分,自认为关键就是控制好精度eps,以r-l>eps为循环条件,每次根据在mid上的判断选择r=midl=mid分支之一即可

一般需要保留k位小数时,则取 eps=1e-(k+2)

有时精度不容易确定或表示,就直接采用循环固定次数的二分方法

  • 二分法的常见模型
  1. 二分答案:最大值最小或最小值最大这类的双最值问题常选用二分答案求解,将最优性问题转换为判定性问题

  2. 二分查找:用具有单调性的布尔表达式求解分界点,比如在有序序列中求数字x的排名

  3. 代替三分(基本不用吧..)


二分答案题目(未完)

  1. 洛谷P2249 【深基13.例1】查找 (难度普及-)

  2. 洛谷P1873 砍树 (难度普及/提高-)

  3. 洛谷P2440 木材加工 (难度普及/提高-)

  4. 洛谷P2920 [USACO08NOV]Time Management S (难度普及/提高-)

  5. 洛谷P2678 跳石头 (难度普及/提高-)

  6. 洛谷P3853 [TJOI2007]路标设置 (难度普及+/提高)

  7. 洛谷P2658 汽车拉力比赛 (难度普及+/提高)

  8. 洛谷P3743 kotori的设备 (难度提高+/省选-)


二分答案题解(未完)

思路中规中矩,想提一下的就是注意二分的循环条件:l<r

如果写成l<=r则会造成死循环,导致第四个点RE(最开始一直以为数组开小了,后面下载了数据才发现死循环了QAQ)

直接给出代码:

#include <bits/stdc++.h>
using namespace std;
long long n,m,l,r=-1,ans,a[2000010];

inline bool check(long long x) {
	register long long sum=0;
	for(register int i=1;i<=n;i++) {
		sum+=a[i]/x;
		if(sum>=m) return true;  //如果当前已经满足,可以不再计算直接返回true,节约时间
	}
	return false;
}

int main() {
	scanf("%lld%lld",&n,&m);
	for(register int i=1;i<=n;i++) {
		scanf("%lld",&a[i]);
		r=max(r,a[i]);
	}
	while(l<r) {  //注意循环条件
		long long mid=(l+r+1)>>1;
		if(check(mid)==true) {
			ans=mid;
			l=mid+1;
		}
		else r=mid-1;
	}
	printf("%lld",ans);
	return 0;
}

看到这道题,首先想到的时贪心算法中的一道题:“智力大冲浪”(带限期与罚款的贪心问题)

那道题的思路就是将任务按结束时间从早到晚排序(从小到大),然后检查限期内是否能够完成,不能则扣钱

转换到这道题,也是将任务先从小到大排序,然后进入二分枚举答案

代码如下:

#include <bits/stdc++.h>
using namespace std;
int n,u,v,l,r,ans=-1;

struct node {
	int st,end;
} a[200010];

inline bool cmp(node x,node y) { //按结束时间从小到大排序
	return x.end<y.end;
}

inline bool check(int x) {
	for(register int i=1;i<=n;i++) {
		if(x+a[i].st<=a[i].end) x+=a[i].st;  //上一个任务结束的时间+当前任务的持续时间是否在当前任务的结束时间之内
		else return false;
	}
	return true;
}

int main() {
	scanf("%d",&n);
	for(register int i=1;i<=n;i++) {
		scanf("%d%d",&a[i].st,&a[i].end);
	}
	sort(a+1,a+1+n,cmp);
	l=0;r=1000000;  //最大限度就是1000000
	while(l<=r) {
		int mid=(l+r)>>1;
		if(check(mid)==true) {
			ans=mid;
			l=mid+1;
		}
		else r=mid-1;
	}
	printf("%d",ans);
	return 0;
}

最开始没什么思路就暴搜来模拟跳石头,但是只得到了可怜的10pts

转换一下思路,我们要求的这个所谓的最短跳跃距离显然在一个范围内,而这个范围题目上已经给了出来1≤L≤1000000000,所以我们可以枚举答案!

怎么枚举?for循环肯定也会超时,那么我们可以使用二分啊,时间复杂度一下就降低了,能够满足这道题

代码如下:

#include <bits/stdc++.h>
using namespace std;
int L,n,m,ans,a[500010];

inline bool check(int x) {
	int k=1,now=0,tot=0; //k是下一块石头,now是当前的石头,tot记录搬走的石头数,我们模拟跳石头过程
	while(k<=n+1&&now<=n+1) { //注意终点不是n而是n+1
		if(a[k]-a[now]<x) tot++;  //如果小于枚举的答案,就搬走
		else now=k;
		k++;
        //优化:if(tot>m) return false;
	}
	if(tot<=m) return true;
	else return false;
}

int main() {
	scanf("%d%d%d",&L,&n,&m);
	for(register int i=1;i<=n;i++) {
		scanf("%d",&a[i]);
	}
	a[n+1]=L;
	int l=0,r=L;  //答案的范围一定在L中
	while(l<=r) {
		int mid=(l+r)>>1;
		if(check(mid)==true) {
			ans=mid;
			l=mid+1;
		}
		else r=mid-1;
	}
	printf("%d",ans);
	return 0;
}

这道题跟跳石头有些许的神似:跳石头是搬走,路标设置是搬来

所以也是通过二分答案来解决,不过这题二分答案的时候需要注意缩小范围的写法:r=mid,l=mid+1,因为是要使得公路的“空旷指数”最小

代码如下:

#include <bits/stdc++.h>
using namespace std;
int L,l,r,n,k,a[200010];

inline bool check(int x) {
	int tot=0;
	for(register int i=0;i<=n;i++) {
		if(a[i+1]-a[i]>x) { //如果大于枚举的答案
			tot+=(a[i+1]-a[i]-1)/x;  //就要添加路标
		}
		if(tot>k) return false;  //超过最大添加量就返回false
	}
	return true;
}

int main() {
	scanf("%d%d%d",&L,&n,&k);
	for(register int i=1;i<=n;i++) {
		scanf("%d",&a[i]);
	}
	a[0]=0;a[n+1]=L;
	l=0;r=L;
	while(l<r) {
		int mid=(l+r)>>1;
		if(check(mid)==true) r=mid;  //注意缩小范围的写法
		else l=mid+1;
	}
	printf("%d",l);
	return 0;
}

请见:汽车拉力比赛 题解 多种做法讲解呀qvq

看着挺复杂的,但是将题意整理在草稿本上就很清晰了:

给定n个设备的用电速度(秒为单位)和初始电量,再给定一个充电器的充电速度(秒为单位),充电器在任意时刻可以给任意装备充电

所有设备同时使用,要求在任意一个装备电量为0之前的最长使用时间

但是值得注意的是:本题明显是在实数域上进行二分答案,所以控制精度eps就显得十分重要(详见上面的“实数域上的二分法”部分)

代码如下:

#include <bits/stdc++.h>
using namespace std;
int n,a[200010],b[200010];
double p,sum,l=0.0,r=1e10;

inline bool check(double x) { 
	double tot=0,q=p*x;
	for(register int i=1;i<=n;i++) {
		if(b[i]>=a[i]*x) continue;  //初始电量满足不管
		else tot+=a[i]*x-b[i];  //否则充电
		if(tot>q) return false;  //如果当前充电量>充电器的充电量,肯定不合法
	}
	return true;
}

int main() {
	cin>>n>>p;
	for(register int i=1;i<=n;i++) {
		scanf("%d%d",&a[i],&b[i]);
		sum+=a[i];
	}
	if(p>=sum) { //如果所有用电器的用电速度和≤充电器速度,肯定可以无限使用
		printf("-1");
		return 0;
	}
	while(r-l>1e-4) {  //控制精度在1e-x(x≤6)
		double mid=(l+r)/2;
		if(check(mid)==true) l=mid;
		else r=mid;
	}
	printf("%.10lf",l);  //根据样例输出
	return 0;
}

To be continued....

posted @ 2020-06-24 15:11  Eleven谦  阅读(408)  评论(0编辑  收藏  举报