• [总结]二分法(二分查找)



    一、关于二分法

    二分法是一种很普通却又很重要的算法。二分法能为我们解题时提供很大的帮助。

    1. 使用前提

    二分法的适用条件是序列具有二分性,也就是单调性。当序列具有二分性,这时我们不断枚举区间中点才能判断这个值是否题设条件。
    当题目中出现诸如最大值的最小最小值的最大的问题时,答案具有二分性。

    2. 分类

    从二分的对象来分类,我们既可以二分最终的答案,我们也可以二分进行判断。
    从二分的类型来分类,可以分为整数域上的二分,以及实数域上的二分

    3. 易错点

    二分法简单易写,但是却很容易写错。我们有很多方法实现二分,而其中的细节地方需要仔细考虑。
    对于整数域上的二分:
    我们需要注意终止条件,左右区间位置的变化,避免错过答案或造成死循环。
    对于实数域上的二分:
    我们需要注意精度的控制。
    建议自己形成固定的代码模型,避免造成不必要的错误。

    4. 二分法的延伸

    C++ STL中的lower_boundupper_bound也可以解决实现在一个序列中二分查找某个整数k的后继。
    二分法能够解决单调问题,进一步地,我们可以扩展二分法至三分法。此时三分法可以解决单峰函数的极值问题。

    二、整数域上的二分

    1. 模板

    在这里给出一种常见的模板:

    while(l<=r){
    	int mid=(l+r)>>1;
    	if(check(mid)){
    		ans=mid;
    		r=mid-1;
    	}
    	else l=mid+1;
    }
    

    三、实数域上的二分

    1. 模板

    实数域上的二分相对简单,只要r-l到达我们所需的精度即可。

    #define eps 1e-5
    while(r-l>eps){
        double mid=(l+r)/2;
        if(check(mid)) r=mid;
        else l=mid;
    }
    

    当我们不确定精度的时候,我们可以采用循环固定次数的形式进行计算。一般这种方式得到的结果的精度比设置的eps更高:

    for(int i=1;i<=100;i++){
        double mid=(l+r)/2;
        if(check(mid)) r=mid;
        else  l=mid;
    }
    

    四、练习

    例1:#9100055「一本通 1.2 例 1」愤怒的牛 / SP297 AGGRCOW - Aggressive cows / P1316 丢瓶盖

    分析:
    很基础的二分,每次二分牛的间隔,如果能放下这c头牛,那么继续扩大这个距离,否则缩小这个距离,直到找到答案。
    代码如下:

    #include<bits/stdc++.h>
    using namespace std;
    int f[1000050],n,c,rem;
    int judge(int x){
        int num=0;
        int temp=f[1]; 
        for(int i=2;i<=n;i++){
            if(f[i]-temp<x) num++;
            else temp=f[i];
            if(num>rem) return 0;
        }
        return 1;
    }
    int main()
    {
    	scanf("%d%d",&n,&c);
    	for(int i=1;i<=n;i++) scanf("%d",&f[i]);
    	sort(f+1,f+n+1);
    	rem=n-c;
    	int maxn=0;
    	int l=1,r=f[n]-f[1];
    	while(l+1<r){
        	int mid=(l+r)/2;
        	if(judge(mid)) l=mid; 
        	else r=mid;
      	}
      	printf("%d
    ",l);
    	return 0;
    }
    

    例2:P1661 扩散

    分析:
    并查集+二分答案。二分枚举形成一个连通块的时间,每次使用并查集统计,如果最后集合的数量大于1,那么移动左区间,否则移动右区间。注意两个点都会扩张,因此单位时间会走双倍的距离。
    代码如下:

    #include<bits/stdc++.h>
    #define N 100010
    using namespace std;
    int sx[N],sy[N],pre[N],n;
    int Find(int x){
    	return (pre[x]==x)? x:Find(pre[x]);
    }
    int check(int mid){
    	for(int i=1;i<=n;i++) pre[i]=i;
        for(int i=1;i<=n;i++){
            for(int j=i+1;j<=n;j++){
                int dis=abs(sx[i]-sx[j])+abs(sy[i]-sy[j]);
                if(mid*2>=dis){
                    int fi=Find(i);
                    int fj=Find(j);
                    if(fi!=fj) pre[fi]=fj;
            	}
            }
        }
        int cnt=0;
        for(int i=1;i<=n;i++) if(pre[i]==i) cnt++;
    	return (cnt==1)? 1:0;
    }
    int main()
    {
        scanf("%d",&n);
        for(int i=1;i<=n;i++)
    		scanf("%d%d",&sx[i],&sy[i]);
        int l=0,r=1e9,ans=0;
        while(l<=r){
            int mid=(l+r)>>1;
            if(check(mid)){
                r=mid-1;
                ans=mid;
            }
            else l=mid+1;
        }
        printf("%d",ans);
        return 0;
    }
    

    例3:P1182 数列分段 Section II

    分析:
    二分枚举每段总和为mid时是否可行,分了超过m段就更新左区间,否则更新右区间。
    代码如下:

    #include<bits/stdc++.h>
    using namespace std;
    int a[100010];
    int n,m,ans,l,r;
    int judge(int mid){
    	int sum=0,cnt=1;
    	for(int i=1;i<=n;i++){
    		if(sum+a[i]<=mid)
    			sum+=a[i];
    		else{
    			sum=a[i];
    			cnt++;
    		}
    	}
    	return (cnt<=m)?1:0;
    }
    int main()
    {
    	scanf("%d%d",&n,&m);
    	for(int i=1;i<=n;i++){
    		scanf("%d",&a[i]);
    		l=max(l,a[i]);
    		r+=a[i];
    	}
    	while(l<=r){
    		int mid=(l+r)>>1;
    		if(judge(mid)){
    			ans=mid;
    			r=mid-1;
    		}
    		else l=mid+1;
    	}
    	printf("%d",ans);
        return 0;
    }
    

    例4:POJ 2018 Best Cow Fences

    分析:
    实数域上的二分。因为平均值只是描述数与数的离散关系,所以我们同时加或减对整个序列的平均值都没有影响。所以我们对序列减去平均值后,问题化为存不存在这样的序列使得区间和大于0。
    我们在(O(N))复杂度内使用前缀和做减法处理出全序列中最大的一段子序列,如果此时序列和小于0,那么我们枚举的平均值过大,因此缩进右区间,反之同理。

    #include<bits/stdc++.h>
    #define N 100010
    #define INF 1e10
    using namespace std;
    double a[N],b[N],sum[N];
    int main()
    {
    	int n,len;
    	scanf("%d%d",&n,&len);
    	for(int i=1;i<=n;i++) scanf("%lf",&a[i]);
    	double l=-1e6,r=1e6;
    	double dlt=1e-5;
    	while(r-l>dlt){
    		double mid=(l+r)/2;
    		for(int i=1;i<=n;i++) b[i]=a[i]-mid;//削去平均值 
    		for(int i=1;i<=n;i++) sum[i]=sum[i-1]+b[i];//求前缀和 
    		double ans=-INF,temp=INF;
    		for(int i=len;i<=n;i++){
    			temp=min(temp,sum[i-len]);//因为长度大于等于L,所以确定一个min左端点 
    			ans=max(ans,sum[i]-temp);
    		}
    		if(ans>=0) l=mid;//可以达到该平均值 
    		else r=mid;
    	}
    	printf("%d",int(r*1000));
    	return 0;
    }
    

    例5:CF670C Cinema

    分析:
    贪心思想,二分枚举每场电影能听懂配音的人数以及看懂字幕的人数,首先满足听懂配音人数,其次满足看懂字幕的人数。

    #include<bits/stdc++.h>
    #define N 200010
    using namespace std;
    inline void read(int &x){
    	x=0;int flag=1;char ch=getchar();
    	while(ch<'0'||ch>'9'){
    		if(ch=='-') flag=-1;
    		ch=getchar();
    	}
    	while(ch>='0'&&ch<='9'){
    		x=(x<<1)+(x<<3)+ch-'0';
    		ch=getchar();
    	}
    	x*=flag;
    }
    int n,m,a[N],b[N],v,pow1,pow2,last1,last2;
    int ans=1;
    int main()
    {
    	read(n);
    	for(int i=1;i<=n;i++) read(a[i]);
    	sort(a+1,a+n+1);read(m);
    	for(int i=1;i<=m;i++) read(b[i]);
    	for(int i=1;i<=m;i++){
    		read(v);
    		int pow1=(upper_bound(a+1,a+n+1,b[i])-a-1)-(lower_bound(a+1,a+n+1,b[i])-a-1);
    		int pow2=(upper_bound(a+1,a+n+1,v)-a-1)-(lower_bound(a+1,a+n+1,v)-a-1);
    		if(pow1>last1||(pow1==last1&&pow2>last2)) last1=pow1,last2=pow2,ans=i;
    	}
    	printf("%d",ans);
    	return 0;
    }
    

    例6:POJ3579 Median

    代码如下:

    #include<iostream>
    #include<cstdlib>
    #include<cstdio>
    #include<algorithm>
    const int N=1e6;
    using namespace std;
    int a[N],n,m,ans;
    int check(int val)
    {
        int cnt=0;
        for(int i=1;i<=n;i++)
    		cnt+=n-(lower_bound(a+1,a+n+1,a[i]+val)-a-1);
        if(cnt>m) return 1;
        else return 0;
    }
    int main()
    {
        while(~scanf("%d",&n))
        {
    		m=n*(n-1)/4;
    		ans=-1;
    		for(int i=1;i<=n;i++)
    			scanf("%d",&a[i]);
    		sort(a+1,a+n+1);
    		int l=1,r=a[n]-a[1];
    		while(l<=r){
    	    	int mid=(l+r)>>1;
    	    	if(check(mid)){
    				ans=mid;
    				l=mid+1;
    	    	}
    	    	else r=mid-1;
    		}
    		printf("%d
    ",ans);
        }
    	return 0;
    }
    

    例7:P1083 借教室

    分析:
    一道很好的思维题,运用了差分的思想。我们在第i天借了k个教室时,在这个时间节点累加这k个教室,在第j天归还的时候再减去。这样我们就能知道任意一天借出教室的数量。我们不断二分这个不能满足的日期,如果最后结果为m,那么全都能满足。否则二分终止的位置就是不能满足的日期。
    代码如下:

    #include<bits/stdc++.h>
    using namespace std;
    int num[1000010],day[1000010];
    int m,n,l[1000010],r[1000010],req[1000010];
    int judge(int mid){
    	memset(day,0,sizeof(day));
    	for(int i=1;i<=mid;i++){
    		day[l[i]]+=req[i];
    		day[r[i]+1]-=req[i];
    	}
    	if(day[1]>num[1]) return 0;
    	for(int i=2;i<=n;i++){
    		day[i]+=day[i-1];
    		if(day[i]>num[i]) return 0;
    	}
    	return 1;
    }
    int main()
    {
    	scanf("%d%d",&n,&m);
    	for(int i=1;i<=n;i++) scanf("%d",&num[i]);
    	for(int i=1;i<=m;i++) scanf("%d%d%d",&req[i],&l[i],&r[i]);
    	int L=1,R=m,ans=0;
    	while(L<=R){
    		int mid=(L+R)>>1;
    		if(judge(mid)){
    			L=mid+1;
    		}
    		else R=mid-1,ans=mid;
    	}
    	if(R!=m) printf("-1
    %d",ans);
    	else printf("0");
    	return 0;
    }
    

    例8: WILL吃桃

    【 题目描述】
    Will 很喜欢吃桃, 某天 Will 来到了一片森林, 森林中有 N 颗桃树, 依次编号为 1,2,„,N.每棵树上有数量不等的桃子。 某些桃树之间有单向通行的小路, 且路径不会形成环, 通过每条小路的时间也不一定相同。 现在, Will 提着一个最多可以容纳 K 个桃子的篮子, 从编号为1 的桃树出发, 走过若干条小路之后来到编号为 N 的桃树。 当 Will 在路上走的时候, 每走 1分钟, 他会从篮子中拿出一个桃子来吃掉( 如果篮子中还有桃子的话, 如果篮子中没有桃子的话那就没得吃了!)。 每到一棵桃树( 包括起点和终点), 他会把这棵桃树上的所有桃子摘下来放入篮子中。 现在你的问题是: 求 K 的最小值, 使得 Will 能够不浪费任何桃子( 每到一棵桃树, 这棵树上的所有桃子都必须被装入篮子中)。

    【 输入格式】
    输入文件第一行两个整数, N,m, 分别表示桃树的数量以及连接桃树的小路的数量。
    接下来一行 N 个用空格隔开的整数, 分别表示每一颗桃树上的桃子的数量。
    接下来 m 行, 每行 3 个用空格隔开的整数, a,b,c, 表示有一条小路能够从桃树 a 走到桃树 b,( 注意小路一定是单向的), 走过这条小路所需要的时间是 c 分钟。从任意一棵桃树出发, Will 不可能沿着小路走若干条路之后重新回到这棵桃树。( 给出的图是一个有向无环图。) 数据保证 Will 一定能够从桃树 1 走到桃树 N。

    【 输出格式】
    输出文件有且仅有一行, 一个整数, 表示 K 的最小值

    【 输入样例】
    3 3
    5 1 6
    1 3 1
    1 2 4
    2 3 5
    【 输出样例】
    6
    【 数据规模】
    对于 30%的数据: 3≤N≤10; m≤20;
    对于 60%的数据: 3≤N≤1,000; m≤10,000;
    对于 100%的数据: 3≤N≤10,000; 3≤m≤30,000; 所有其他数据都不超过 10000;

    分析:
    题目要求求出篮子容量的最小值K,我们很容易知道如果篮子容量小于K无法满足,如果容量大于K则会有剩余的空间而不是最优的答案。因此K的取值具有二分性,我们二分这个K并跑最短路(以篮子中的桃子作为权值),如果最终篮中的桃子数少于mid,那么缩小右区间,否则缩小左区间。

    具体详见代码:

    #include<bits/stdc++.h> 
    #define N 100010
    #define INF 0x3f3f3f3f
    using namespace std;
    int n,m,tot;
    int first[N],nxt[N],go[N],cost[N],fil[N];//fil每个节点装入桃子的数量 
    int vis[N],dist[N];
    inline void add_edge(int u,int v,int w){
    	nxt[++tot]=first[u];
    	first[u]=tot;
    	go[tot]=v;
    	cost[tot]=w;
    }
    int check(int mid){
    	queue<int> q;//最短路 
    	for(int i=1;i<=n;i++) vis[i]=0,dist[i]=INF;
    	vis[1]=1;
    	dist[1]=fil[1];
    	q.push(1);
    	while(!q.empty())
    	{
    		int u=q.front();
    		q.pop();
    		vis[u]=0;
    		for(int e=first[u];e;e=nxt[e]){
    			int v=go[e],w=cost[e];
    			int add=fil[v];
    			int rest=(dist[u]-w>0)?(dist[u]-w):0;//可能会桃子不够吃的情况 
    			if(add+rest>mid) continue;//装不下,不能选这条道 
    			if(add+rest<dist[v]){//松弛 
    				dist[v]=add+rest;
    				if(!vis[v]){
    					q.push(v);
    					vis[v]=1;
    				}
    			}
    		}
    	}
    	return (dist[n]<=mid)? 1:0;
    }
    int main()
    {
    	scanf("%d%d",&n,&m);
    	for(int i=1;i<=n;i++)
    		scanf("%d",&fil[i]);
    	for(int i=1;i<=m;i++){
    		int u,v,w;
    		scanf("%d%d%d",&u,&v,&w);
    		add_edge(u,v,w);
    	}
    	int l=fil[1],r=1e8,ans;
    	while(l<=r){
    		int mid=(l+r)>>1;
    		if(check(mid)){
    			ans=mid;
    			r=mid-1;
    		}
    		else l=mid+1;
    	}
    	printf("%d",ans);
    	return 0;
    }
    

    例9: 【codevs 3342】绿色通道Green Passage

    题目描述 Description
    《思远高考绿色通道》(Green Passage, GP)是唐山一中常用的练习册之一,其题量之大深受lsz等许多oiers的痛恨,其中又以数学绿色通道为最。2007年某月某日,soon-if (数学课代表),又一次宣布收这本作业,而lsz还一点也没有写……
    高二数学《绿色通道》总共有n道题目要写(其实是抄),编号1..n,抄每道题所花时间不一样,抄第i题要花a[i]分钟。由于lsz还要准备NOIP,显然不能成天写绿色通道。lsz决定只用不超过t分钟时间抄这个,因此必然有空着的题。每道题要么不写,要么抄完,不能写一半。一段连续的空题称为一个空题段,它的长度就是所包含的题目数。这样应付自然会引起马老师的愤怒。马老师发怒的程度(简称发怒度)等于最长的空题段长度。
    现在,lsz想知道他在这t分钟内写哪些题,才能够尽量降低马老师的发怒度。由于lsz很聪明,你只要告诉他发怒度的数值就可以了,不需输出方案。(快乐融化:那么lsz怎么不自己写程序?lsz:我还在抄别的科目的作业……)

    输入描述 Input Description
    第一行为两个整数n,t,代表共有n道题目,t分钟时间。
    以下一行,为n个整数,依次为a[1], a[2],... a[n],意义如上所述。

    输出描述 Output Description
    仅一行,一个整数w,为最低的发怒度。

    样例输入 Sample Input
    17 11
    6 4 5 2 5 3 4 5 2 3 4 5 2 3 6 3 5

    样例输出 Sample Output
    3

    数据范围及提示 Data Size & Hint
    60%数据 n<=2000
    100%数据 0<n<=50000,0<a[i]<=3000,0<t<=100000000

    分析:
    让你求出最小的发怒值。本题继承上一道题的思想,可以确定发怒值具有二分性,那么如何写check函数呢?
    我们很容易想到动态规划,由于每个作业的区间存在重叠关系,因此可以使用单调队列优化。设(f(i))数组表示在完成作业(i)时要花费的最小总时间,我们维护一个单调上升的队列,当扫描到的数所花的时间比队尾大,那么不断删去队尾并插入这个数来保证每次队头的元素的值最小;在取队头前需要检查队头元素是否超出区间,超出的元素需要删去。
    Code:

    #include<bits/stdc++.h>
    #define INF 0x3f3f3f3f
    #define N 50010
    using namespace std;
    int n,t,a[N],f[N],q[N];
    int check(int val)
    {
        memset(f,0x3f,sizeof(f));
        memset(q,0,sizeof(q));
    	f[0]=0;
        int head=0,tail=0;
        tail++;
        for(int i=1;i<=n;i++)//单调上升队列 
        {
            while(head<=tail&&q[head]<i-val-1)//(多减一位,相当于i与选的值中间差了val(即mid))
    			head++;//超出范围的元素要删除 
            f[i]=f[q[head]]+a[i];//更新时间 
            while(head<=tail&&f[q[tail]]>=f[i]) tail--;//删除不是最优的队尾 
            q[++tail]=i;//加入新的位置 
        }
        int ans=INF;
        for(int i=n-val-1;i<=n;i++)
            ans=min(ans,f[i]);
        //若有时间小于t,则该愤怒值枚举d大了 
        if(ans<=t) return 1;//更新右区间 
        return 0;
    }
    int main()
    {
    	scanf("%d%d",&n,&t);
    	for(int i=1;i<=n;i++)
    		scanf("%d",&a[i]);
    	int l=0,r=n,ans=0;
    	while(l<=r){
    		int mid=(l+r)>>1;
    		if(check(mid)){
    			ans=mid;
    			r=mid-1;
    		}
    		else l=mid+1;
    	}
    	printf("%d",ans);
    	return 0;
    }
    

    例10: P1314 聪明的质监员

    前缀和优化,check函数:“从Li到Ri,中间的所有w值超过W的项的个数乘上这些矿石的v的和”。
    我们二分枚举参数W,并求出最终的Y,寻找(min(abs(S-Y)))意味着S与Y的距离最近,因此只要Y大于S就更新l,否则更新r,这样就可以向S不断逼近。
    Code:

    #include<bits/stdc++.h>
    #define N 200010
    #define INF 0x7f7f7f7f
    #define ll long long
    using namespace std;
    ll res,n,m,s,w[N],val[N],y,a[N],b[N];
    ll sumn[N],sumv[N],L=INF,R=0;
    inline bool judge(ll mid){
    	y=0;
    	memset(sumv,0,sizeof(sumv));//满足条件矿石的价值
    	memset(sumn,0,sizeof(sumn));//满足条件矿石的个数
    	for(int i=1;i<=n;i++){
    		sumn[i]=sumn[i-1],sumv[i]=sumv[i-1];
    		if(w[i]>=mid) sumv[i]+=val[i],sumn[i]++;//符合条件
    	}
    	for(int i=1;i<=m;i++)
    		y+=(sumn[b[i]]-sumn[a[i]-1])*(sumv[b[i]]-sumv[a[i]-1]);//求出Y
    	res=(ll)abs(y-s);//求出当前答案
    	if(y>s) return 1;//W可以更小一些
    	else return 0;//W可以更大一些
    }
    int main()
    {
    	scanf("%lld%lld%lld",&n,&m,&s);
    	for(int i=1;i<=n;i++){
    		scanf("%lld%lld",&w[i],&val[i]);
    		L=min(w[i],L);R=max(w[i],R); 
    	}
    	for(int i=1;i<=m;i++) scanf("%lld%lld",&a[i],&b[i]);
    	ll l=0,r=R+2,ans=0x3f3f3f3f3f3f3f3f;
    	while(l<=r){
    		ll mid=(l+r)>>1;
    		if(judge(mid)) l=mid+1;
    		else r=mid-1;
    		ans=min(ans,res);
    	}
    	printf("%lld",ans);
    	return 0;
    }
    

    例11: P1281 书的复制

    二分枚举完成的时间,求出最优时间后用递归求出最终每个人负责的范围。
    细节见代码。
    Code:

    #include<bits/stdc++.h>
    using namespace std;
    int n,k,a[600],l,r,ans,cnt;
    inline int check(int time){
    	int per=0;cnt=1;//初始化
    	for(int i=1;i<=n;i++){
    	    if(a[i]>time) return 0;//不可能完成该任务
    		if(per+a[i]<=time) per+=a[i];//贪心,即让每个人在限制时间内尽量多地抄写
    		else cnt++,per=a[i];//超出时间让下一个人做
    	}
    	return cnt<=k;//是否满足限制人数
    }
    inline void print(int x,int y){
    	int per=0;
    	for(int i=y;i>=x;i--){
    		if(per+a[i]>ans){
    			print(x,i);//继续划分1~i
    			printf("%d %d
    ",i+1,y);//i+1~y是一段
    			return;
    		}
    		per+=a[i];
    	}
    	printf("%d %d
    ",x,y);
    	return;
    }
    int main()
    {
    	scanf("%d%d",&n,&k);
    	if(!n) return 0;//数据会出现n=0,k=0的情况 
    	for(int i=1;i<=n;i++){
    		scanf("%d",&a[i]);
    		r+=a[i];
    	}
    	while(l<=r){//二分求出最小时间
    		int mid=(l+r)>>1;
    		if(check(mid)){
    			r=mid-1;
    			ans=mid;
    		}
    		else l=mid+1;
    	}
    	print(1,n);
    	return 0;
    }
    

    pic.png

  • 相关阅读:
    System Verilog 片断
    如何避免covergroup中出现错误
    一种FPGA图像处理算法的快速验证方式
    什么才是一个feature to be test?
    我的第一份vPlan衍变路线
    思想误区解答:请专注于DUT的功能(全部为菜鸟个人总结不保证正确)
    谈谈验证中的SystemVerilog和CPP//转载
    ResourceBundleViewResolver
    springmvc json数据返回前台,中文乱码
    将字符串中间的某段长度替换成固定的值
  • 原文地址:https://www.cnblogs.com/cyanigence-oi/p/11729941.html
Copyright © 2020-2023  润新知