浅谈二分答案

〇、引子

二分是什么?

就是分而治之

举个例子,假设某一天你被 JC 了,幸运的是,机房后面有一个监控,它记录了你电脑上的界面,你想知道什么时候你被 JC ,那你应该怎么找呢? 你可以一秒一秒的查,这非常慢,但是入果你使用二分的思想,选择中间的时间,如果已经被 JC 了,就往前找,如果没有,就往后找,然后再取中间值,不停下去,就可以很快地找到这个时间。


 一、简述


二分,大家应该都知道,就是通过折半查找来进行枚举。

而二分答案就是就相当于枚举答案,并判断答案是否合法,如果合法,就将答案进一步靠近,如果不合法,就往前判断。对于每个次判断,即使复杂度较高,也可以稳过。

大概就是这个思路:(绿色表示符合条件,灰色表示不符合)

 

 

 

 

复杂度?O(\log n) 不是显而易见的吗???


 二、范围

二分答案可以用在什么地方呢?

显然,每次判断都会返回一个布尔值,这个值判断这个答案是大了还是小了,所以只有答案具有单调性的时候才可以用二分答案。还可以通过找“最大值最小”或“最小值最大”这种字眼来判断能不能使用二分。

三、模板

ll l=0,r=100000001,mid;//定义最小边界、最大边界。
while (l+1<r) {//判断结束。
    mid=(l+r)/2;//找中间值。
    if(f(mid)) l=mid;//移动左端点。
    else r=mid;//移动右端点。
}

二分答案最重要的就是写判断函数,其中要尤其注意当判断答案的大小要注意等于的情况。

当然,你也可以在循环中就直接判断等于的情况。

四、例题

先来点简单的:

 1.P2440 木材加工 

求一个最大值并能在所有 a_i 中取出 k 段。

这个题可以二分 ans ,然后将每一个 a_i 都枚举一遍,取出每一个 a_i 能分出几个 ans 并求出总和与 k 比较。如果大于 k 就说明 ans 太小了,反之就是太大了。 

核心代码:

bool f(ll ans) {
    ll sum = 0;
    for (int i=1;i<=n;i++) {
        sum+=a[i]/ans;
    }
    return sum>=k;
}
ll l=0,r=100000001,mid;
while (l+1<r) {
    mid=l+r>>1;
    if(f(mid)) l=mid;
    else r=mid;
}
cout<<l<<endl;


 2. P2678 跳石头

在 a_i 中去掉 M 个点,使点的间隔的最小值最大,求这个最大值。

这个题我们看到了标志“间隔的最小值最大”,就说明可以直接二分答案。我们二分一个 ans,并记录出间隔小于 ans 的个数,最后与 M 比较,如果小于等于,就说明大了,反之就是小了。

核心代码:

bool F(int x){
    ll i,k=0,ans=0;
    for(i=1;i<=n;i++){
        if(a[i]-k<x) ans++;
        else k=a[i];
    }
    return ans<=m;
}
ll l=1,r=L+1,mid;
while(left+1<right){
    mid=(left+right)/2;
    if(F(mid)) l=mid;
    else r=mid;
}
cout<<l;

稍微难一点的:

3.P3853 路标设置

在区间 [0,L] 中放置路标,使两个路标之间的差的最大值最小。(已经有一些路标)

又是最大值最小,继续二分,这个判断函数怎么写呢?对于每两个已经放置好的路标,我们只需要除以 ans 就可以了,但如果正好整除,那就把总数减一。

核心代码:
 

bool F(int x){
    int num=0;
    for(int i=2;i<=n;i++){
        if(a[i]-a[i-1]>=x){
            num+=(a[i]-a[i-1])/x;
            if((a[i]-a[i-1])%x==0)
                num--;
        }
    }
    return num>k;
}

再难点:


4.P1948 [USACO08JAN]Telephone Lines S

终于到蓝题了(

将 p 对电线杆相连,并接到第 1 和第  个电线杆上,其中  对是免费的,剩下的费用是最长的电线的长度,求最小值。

这个题要结合最短路。我们二分最小费用,然后对于每一个费用,求出 1 到 n最小需要免费几条线,最后和 k 比较。

代码:

// Problem: P1948 [USACO08JAN]Telephone Lines S
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P1948
// Memory Limit: 125 MB
// Time Limit: 1000 ms
// 
// Powered by CP Editor (https://cpeditor.org)

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define M 100005
#define INF 0x3f3f3f3f
int n,p,k;
int ft[M],nt[M],v[M],w[M],idx=1,cnt;
void add(int a,int b,int c){
    v[idx]=b;
    w[idx]=c;
    nt[idx]=ft[a];
    ft[a]=idx++;
}//邻接表存图
bool vis[M];
int dist[M];
int F(int x){
    queue<int>q;
    q.push(1);
    for(int i=2;i<=n;i++) dist[i]=INF;
    vis[1]=1;dist[1]=0;
    while(!q.empty()){//SPFA
        int h=q.front();
        q.pop();
        vis[h]=0;
        for(int i=ft[h];~i;i=nt[i]){//枚举
            int y=v[i],z=w[i];
            if(z<=x) z=0;//判断
            else z=1;
            if(dist[y]>dist[h]+z){
                dist[y]=dist[h]+z;
                if(!vis[y]){
                    vis[y]=1;
                    q.push(y);
                }
            }
        }
    }
    if(dist[n]==INF){
        cout<<"-1";
        return -1;//特判
    }
    return dist[n]<=k;
}

int main(){
    memset(ft,-1,sizeof(ft));
    cin>>n>>p>>k;
    for(int i=1;i<=p;i++){
        int a,b,c;
        cin>>a>>b>>c;
        add(a,b,c);
        add(b,a,c);
        cnt+=c;//最大值是权值总和。
    }
    int l=0,r=cnt,mid;
    while(l<=r){
        mid=(l+r)/2;
        if(F(mid)==0) l=mid+1;
        else if(F(mid)==1) r=mid-1;
        else return 0;//很笨的方法判断。
    }
    cout<<l;
    return 0;
}


5.P1163 山

给出一个山(在坐标系中的一个分段函数),在山的某一部位安装一盏灯,求灯的y坐标最小值,使山的所有部位都能被灯照到。

第一眼看这个题目,可能会觉得这题要二分 $x$ 和 $y$。但是分析一下我们知道能放灯的地方只有所有直线的上方:

 

那么我们可以很快联想到要先把所有的直线解析式求出来。

那么我们定义一个 b_i 和 k_i ,存储的是第 i 条直线的解析式(y=kx+b)。

然后呢?然后对于每一个 y 坐标,判断在最大的左端点 L 和最小的右端点 R,就像这样:

 

 

(在高度为y的情况下,x=L 是最大的右端点,x=R 是最小的右端点)

最后判断,如果 R<L 就是不符合条件。

核心代码:

bool F(double x){
    double L=-100005,R=100005;
    for(int i=2;i<=n;i++){
        if(k[i]<0) L=max(L,(x-b[i])/k[i]);
        else if(k[i]>0) R=min(R,(x-b[i])/k[i]);
        else if(x<b[i]) return 0;//特判k等于0的情况。
    }
    return L<=R;
}

6.CF492D Vanya and Computer Game

A 每秒攻击 x 次,B每秒攻击 y 次,第 i 个怪物被攻击 a_i 次就死了,问谁给了每个怪物最后一击。


这题实际上是比例的问题,A 打一次是 \frac{1}{x} 秒 B打一次是 \frac{1}{y} 秒,比例是 \frac{1}{x} :\frac{1}{y},浮点数不好计算,把二者乘以 xy,那么 A 打一次就是 y 秒,B打一次就是 x 秒。然后二分怪物被打死的时间 ans 条件是 \frac{1}{x} + \frac{1}{y}\geq a

注意乘以之后数据范围的变化,已经超过题目所给的 10^9 了,所以二分枚举时间的时候要特别注意。

代码:

while(n--){
    int t;
    cin>>t;
    ll l=-1,r=1e15,mid;
    while(l+1<r){
        mid=(l+r)/2;
        if(mid/x+mid/y>=t) r=mid;
        else l=mid;
    }
    if(r%x==0&&r%y==0) cout<<"Both"<<endl;
    else if(r%x==0) cout<<"Vova"<<endl;
    else cout<<"Vanya"<<endl;
    
}

7.P2218 [HAOI2007]覆盖问题

给出平面上n 个点,求一个最小的 L,使每个点都能被至少一个 L \times L 的正方形覆盖,只有3个正方形。

这个题难点就在与如何判断是否符合条件。因为只有三个正方形,所以我们可以先判断第一个正方形,然后删除已经被覆盖的点,然后对于剩余的点求出第二个正方形,最后在求出第三个,之后用剩余的点与全部的点比较,最后判断是否符合条件。

我的代码太难看了放一篇题解:

https://www.luogu.com.cn/blog/ypcaeh/solution-p2218


五、总结

对于一个考二分答案的题,我们首先要找到最大值最小或最小值最大这种字眼,然后再想可不可以用二分答案。首先把题目反过来,想如何判断答案是否符合条件,然后写出判断函数,最后把模板敲上。

二分答案也经常与其他的算法配合,其实就是对这些算法的优化。当你发现答案有单调性时,就可以考虑是否可以用二分答案。

完结撒花~

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值