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

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

一、关于二分法

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

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\n",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\n",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\n%d",ans);
    else printf("0");
    return 0;
}

猜你喜欢

转载自www.cnblogs.com/cyanigence-oi/p/11729941.html
今日推荐