ACM CSP竞赛笔记(十)——二分查找

参考课程是我高中信息竞赛邱老师的课程。

【11-2二分答案】 https://www.bilibili.com/video/BV1uG411k7e1/?share_source=copy_web\&vd_source=2c56c6a2645587b49d62e5b12b253dca

【11-1二分查找】 https://www.bilibili.com/video/BV1Rb4y1G73U/?share_source=copy_web\&vd_source=2c56c6a2645587b49d62e5b12b253dca

完整的板子可以去资源获取,我设置0积分,应该是免费的,如果C收你费或者下不了B站私我。

二分查找(寻找一个数/最大值最小值 必须有序)

只分第一次和最后一次满足。

整数二分

二分满足答案

所有check(p)类的问题都基本满足第一类,只要是递增关系,都是第一个大于T的。

二分一定要分区**"第一次满足"** 和**"最后一次满足"**

比如最后一次满足,说明是找区间右侧的值,要压缩左边界。

比如**第一次满足,**说明是找区间左侧的值,要压缩右边界。

要不你把y>=s y>s y<=s y<s的四种情况都说一下吧

二分法核心

只有两种情况,一种是第一次满足,一种是最后一次满足

这种诡异的模板实际上是 保留法和排除法的混合。

做题应该根据求第一个还是最后一个,然后锁边界/输出。

练习 矿石问题

P1314

https://www.luogu.com.cn/problem/P1314

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
int n,m;
long long s;
//提到了区间和 就是满足某个性质的区间和问题
//重量和价值都会影响区间和,因此需要开两个presum前缀和数组,presum一定要对应一个原始数组,因为迭代的时候肯定要用到presum[i]=presum[i-1]+a[i]
long long presumw[2000005];
long long presumv[2000005];
long long w[2000005];
long long v[2000005];
//同时还要添加检验区间
long long intL[2000005],intR[2000005];
void init(){
    memset(presumw,0,sizeof(presumw));
    memset(presumv,0,sizeof(presumv));
}

long long caly(long long W){
    init();
    for(int i=1;i<=n;i++){
        
        if(w[i]>=W){
            //弄presum
            presumw[i]=presumw[i-1]+1;
            //注意 这里的式子是01,所以是+1不是+w[i]!!!
            presumv[i]=presumv[i-1]+v[i];
        }else{//带性质判断的特殊点就在于 不满足性质的时候也要写,因为是迭代式,不写就断了,这里保持上一个状态即可。
            presumw[i]=presumw[i-1];
            presumv[i]=presumv[i-1];
        }
    }
    //检测M组区间的所有满足性质的值
    long long Y=0;
    for(int i=1;i<=m;i++){
        long long L=intL[i],R=intR[i];
        Y+=(presumw[R]-presumw[L-1])*(presumv[R]-presumv[L-1]);
    }
    return Y;
}
bool check(long long W){
    return caly(W)>=s;
}
int find(){
    long long L=0,R=1e12;
    
    while(R>L){
        long long W=(R+L+1)/2;
        if(caly(W)>=s){//最后一个满足y>=s的 最后一个 L=mid
            L=W;
        }else{
            R=W-1;
        }
    }
    // while(R>L){
    //     long long W=(R+L)/2;
    //     if(caly(W)<s){//第一个满足y<s的 第一个 R=mid
    //         R=W;
    //     }else{
    //         L=W+1;
    //     }
    // }
    //上面输出的是第一个大于s的W 因此应该检查W和W+1的输出
    if(abs(caly(L)-s)<abs(caly(L+1)-s)){
        cout<<abs(caly(L)-s)<<endl;
        return L;
    }else{
        cout<<abs(caly(L+1)-s)<<endl;
        return L+1;
    }

}
int main(){
    cin>>n>>m>>s;
    for(int i=1;i<=n;i++){
        cin>>w[i]>>v[i];
    }
    for(int i=1;i<=m;i++){
        cin>>intL[i]>>intR[i];
    }

    long long result=find();
    //cout<<result<<endl;
    return 0;
}

模板

见上文。

练习

复印机

SDUOJ

//二分变量是时间 T

//判断时间单调性

//确定搜索范围 l r l=0 r=n*min(x,y)

//批准条件check 能在T内完成复印n-1份

二分条件判断:

找到第一个满足if()的,

cpp 复制代码
while(R>L){
        int mid=(L+R)/2;
        if(check(mid)){//如果复印完 时间多
            R=mid;
        }else{
            L=mid+1;
        }
    }
cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
//二分变量是时间 T
//判断时间单调性
//确定搜索范围 l r l=0 r=n*min(x,y)
//批准条件check 完成复印
int x,n,y;
long long max_t=1e9;
bool check(int T){//这个时间是否能复印完n-1件
    long long cnt=T/x+T/y;
    return cnt>=(n-1);
}
int find(){
    long long L=0,R=max_t;
    while(R>L){
        int mid=(L+R)/2;
        if(check(mid)){//如果复印完 时间多
            R=mid;
        }else{
            L=mid+1;
        }
    }
    return L;
}
int main(){
    cin>>n>>x>>y;
    //max_t=n*min(x,y);
    if(n==1) cout<<0<<endl;
    if(n==2){
        cout<<min(x,y)<<endl;
    }else{
    //其他情况只需要复印n-1份,然后有一份直接单独印刷
        long long min_t=find();
        cout<<min_t+min(x,y)<<endl;
    }
    
    return 0;
}

浮点二分(解方程 也需要单调)

方法五步:

①判断搜索变量

②变量是否单调

③const double eps=1e-6 确定误差

④double/int l r 确定搜索范围(可减小)

⑤批准条件check()

一定要注意,是否所有的参与变量都使用double了》

可以预设解的范围缩小

模板

将if(f(mid)*f(r)<=0)换为bool check()函数

用次数截止(查找多少次停止)

练习

SDUOJ

截绳子

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
//可行性判断 绳子长度是可单调的
//double len=(r-l)/2 变量是绳子的长度
//eps 误差是10e-6
//double l r 查找范围是 1,1e7
//批准条件check() 批准条件是能切出k段这么长的绳子
const double eps=1e-6;
int n,k;
vector<int> rope(1e7);
bool check(double len){
    int cnt=0;
    for(int x:rope){
        if(x>=len){//如果能够切
            cnt+=x/len;
        }else{
            break;
        }
    }
    return cnt>=k;//能切够k段吗
}
double find(){
    double l=0,r=1e7;
    while(abs(r-l)>eps){
        double len=(l+r)/2;
        if(check(len)){//如果够切就可以增加长度 
            l=len;
        }else{
            r=len;
        }
    }
    return l;
}

int main(){
    
    cin>>n>>k;
    for(int i=0;i<n;i++){
        cin>>rope[i];
    }
    sort(rope.rbegin(),rope.rend());
    double max_len=find();
    cout<<max_len<<endl;
    return 0;
}