zoukankan      html  css  js  c++  java
  • 浅谈二分

    二分算法(未完)

    前言

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

    所以,二分是必掌握的算法,这篇博客就是我的二分的学习记录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....

  • 相关阅读:
    wget命令
    Linux常用命令大全
    centos7 中文乱码问题解决方法
    Linux软件安装的补充
    redis在Linux上的安装
    Linux下tomcat的安装
    Linux常见命令
    Linux下jdk安装过程
    JAVA中日期处理
    JAVA中File类的使用
  • 原文地址:https://www.cnblogs.com/Eleven-Qian-Shan/p/13187609.html
Copyright © 2011-2022 走看看