二分算法(未完)
前言
二分属于分治的一种,很多题都需要用到这种高效简洁的算法
所以,二分是必掌握的算法,这篇博客就是我的二分的学习记录qwq
目录
- 二分算法知识
- 整数集合上的二分
- 实数域上的二分法
- 二分法的常见模型
-
二分答案题目
-
二分答案题解
-
二分查找题目
-
二分查找题解
二分
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
如上所示,二分写法可能会有两种形式:
- 缩小范围时,
r=mid,l=mid+1
,取中间值时,mid=(l+r)>>1
- 缩小范围时,
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=mid
或l=mid
分支之一即可
一般需要保留k位小数时,则取 eps=1e-(k+2)
有时精度不容易确定或表示,就直接采用循环固定次数的二分方法
- 二分法的常见模型
-
二分答案:最大值最小或最小值最大这类的双最值问题常选用二分答案求解,将最优性问题转换为判定性问题
-
二分查找:用具有单调性的布尔表达式求解分界点,比如在有序序列中求数字x的排名
-
代替三分(基本不用吧..)
二分答案题目(未完)
-
洛谷P2249 【深基13.例1】查找 (难度普及-)
-
洛谷P1873 砍树 (难度普及/提高-)
-
洛谷P2440 木材加工 (难度普及/提高-)
-
洛谷P2920 [USACO08NOV]Time Management S (难度普及/提高-)
-
洛谷P2678 跳石头 (难度普及/提高-)
-
洛谷P3853 [TJOI2007]路标设置 (难度普及+/提高)
-
洛谷P2658 汽车拉力比赛 (难度普及+/提高)
-
洛谷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;
}