二分的总结

这篇文章想通过几个题来感觉一下二分应该怎么分析,如何使用二分算法

对于二分,二分是二分性而不是单调性 只要满足可以找到一个值一半满足一半不满足即可 而不用满足单调性。

https://www.luogu.com.cn/training/545539#problems一个二分题单

我是习惯看到最值考虑二分

本文题解属于拙见,水平有限,理性参考

AcWing503. 借教室

先来看看这个题的描述:

要处理接下来n天的借教室信息,其中第i天有ri个教室可以租借。

一共有m份订单,每个订单包括三个正整数d,s,t,表示从第s天到第t天租界教室,每天需要租d个

如果分配过程中遇到一份订单无法满足就停下来。如果可以满足输出0,否则输出-1 需要修改订单的申请人编号。

我们思考一下,这里订单是按顺序输入的,如果第i个订单可以满足那么前i-1个订单就都可以满足,反之如果第i个订单无法满足呢么之后的订单也就无法满足,这这样看来答案具有单调性。

所以我们可以对答案进行二分。

基本上就是套模板了,我们需要二分出能满足到哪个订单(mid),然后check(mid),验证一下从1~mid这些订单能否被满足,如果满足了,那么就去右边的区间接着找,不满足就去左边的区间找。

在check部分为了降低复杂度我们考虑差分,根据输入的r数组构建差分数组,每次订单操作差分完成。

用res把差分数组累加,进行判断就可以了。

  • res<0,教室数量不够
  • res>0,教室数量足够
#include <bits/stdc++.h>
using namespace std;
const int N=1e6+5;
using ll=long long;//注意到数据比较大,防止爆int
int n,m;
ll r[N];
ll d[N],s[N],t[N];
ll b[N];
bool check(int mid)
{
    for(int i=1;i<=n;i++) b[i]=r[i];
    for(int i=1;i<=mid;i++){//检查所有mid之前(包括mid)的订单
    //这部分是差分的一个基本操作,在一个区间加上租借的教室数量
        b[t[i]+1]+=d[i];
        b[s[i]]-=d[i];
    }
    ll res=0;
    for(int i=1;i<=n;i++)
    {
        res+=b[i];
        if(res<0) return true;//注意这里要返回true
        //如果不满足我去左边找所以要让r=mid
    }
    return false;
    //满足了我要去右边找,所以要让l=mid+
}
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>r[i];
    for(int i=n;i;i--) r[i]-=r[i-1];//构建差分数组
    for(int i=1;i<=m;i++)
    {
        cin>>d[i]>>s[i]>>t[i];
    }
    if(!check(m)){
        cout<<"0";
        return 0;
    }
    int l=0,r=m;
    while(l<r)//这里用的就是尽量往左找的模板,找到第一个不满足订单
    {
        int mid=l+r>>1;
        if(check(mid)) r=mid;
        else l=mid+1;
    }
    cout<<"-1"<<endl<<l;
    return 0;
}

AcWing 1227. 分巧克力

先看看题的描述:

有K个小朋友,N块巧克力,其中第i块是Hi*Wi的方格组成的长方形

需要从N块巧克力中切出K块分给小朋友们。

要满足1.形状是正方形,边长是常数 2.大小相同

要求最大的边长

我们先来想一下巧克力怎么切,一块65的巧克力,我们可以切22的6块,也可以切33的两块,所以当边长为a时,我们可以切(h/aw/a)块

我们可以观察到答案具有单调性(单调减),考虑二分

那check检查什么呢?我们二分正方形边长长度,判断以该长度为边长时能分得的块数是否大于等于k,如果大于等于的话,继续增大边长,否则就减小边长。

#include <bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int h[N],w[N];
int n,k;
bool check(int mid)//判断边长为mid是否能分出k块巧克力
{
    int ans=0;
    for(int i=n;i>0;i--)//个人感觉倒序比正序要快
    {
        ans+=(h[i]/mid)*(w[i]/mid);
        if(ans>=k) return true;
    }
    return false;
}
int main()
{
    cin>>n>>k;
    for(int i=1;i<=n;i++) cin>>h[i]>>w[i];
    int l=1,r=1e5;//注意这里分的是边长,所以l从1开始,r从1e5开始
    //没有虚拟的饼干哈哈哈(⊙﹏⊙)
    while(l<r)//这里用的是尽量往右找的模板,实现最大
    {
        int mid=l+r+1>>1;
        if(check(mid)) l=mid;
        else r=mid-1;
    }
    cout<<l;
    return 0;
}

AcWing 5407. 管道

先看看题的描述:

一根长为len的横向管道,按照单位长度分为len段,每一段的中央有一个可开关的阀门和一个检测水流的传感器。

一开始管道是空的,位于 Li 的阀门会在 Si 时刻打开,并不断让水流入管道。

对于位于 Li 的阀门,它流入的水在 TiTi≥Si)时刻会使得从第 Li−(Ti−Si) 段到第 Li+(Ti−Si) 段的传感器检测到水流。

求管道中每一段中间的传感器都检测到有水流的最早时间

这个题需要复习一下区间合并算法,请读者自行复习或者看我之后发的文章。

首先结果是一个时间肯定符合单调性,二分时间,l从1开始,r从2e9(从1e9开始从第一个位置放水,灌满需要1e9,一共是2e9)开始和上个题类似。

那么问题来了如何进行check?

#include <bits/stdc++.h>
using namespace std;
#define x first
#define y second
using ll=long long;
const int N=1e5+6;
typedef pair<int,int>PII;
PII w[N],q[N];
int n,len;
bool check(ll mid)
{
    int cnt=0;
    for(int i=0;i<n;i++)
    {
        int L=w[i].x,S=w[i].y;
        if(S<=mid)
        {
            int t=mid-S;
            int l=max(1,L-t),r=min((ll)len,(ll)L+t);
            q[cnt++]={l,r};
        }
    }
    sort(q,q+cnt);
    int st=-1,ed=-1;
    for(int i=0;i<cnt;i++)
    {
        if(q[i].x<=ed+1) ed=max(q[i].y,ed);
        else st=q[i].x,ed=q[i].y;
    }
    return st==1&&ed==len;
}
int main()
{
    cin>>n>>len;
    for(int i=0;i<n;i++)
    {
        cin>>w[i].x>>w[i].y;
    }
    ll l=1,r=2e9;
    while(l<r){//这里用的是尽量往左找的模板
        ll mid=l+r>>1;
        if(check(mid)) r=mid;
        else l=mid+1;
    }
    cout<<l;
    return 0;
}

当然如果你觉得区间合并太复杂了,也有第二种做法

#include <bits/stdc++.h>
using namespace std;
using ll=long long;
const int N=1e5+6;
int n,len;
int L[N],S[N];
bool check(ll tim)
{
    int p=0;
    for(int i=1;i<=n;i++)
    {
        if(tim>=S[i])
        {
            if(p>=len) return true;
            int l=L[i]-(tim-S[i]);
            int r=L[i]+(tim-S[i]);
            if(l-1<=p) p=max(p,r);//说明这段区间和之前的区间是可以连接上的
        }
    }
    if(p>=len) return true;
    return false;
}
int main()
{
    cin>>n>>len;
    for(int i=1;i<=n;i++)
    {
        cin>>L[i]>>S[i];
    }
    ll l=1,r=2e9;
    while(l<r){//这里用的是尽量往左找的模板
        ll tim=l+r>>1;
        if(check(tim)) r=tim;
        else l=tim+1;
    }
    cout<<l;
    return 0;
}

插一句话,如果你看到这里累了的话就休息一会,因为我写到这里也累了。

AcWing 4656. 技能升级

先来看看题目描述吧:

角色有N个可以加攻击力的技能,其中第i个技能首次可以提升Ai点攻击力,以后每次升级增加的点数都会减少Bi

Ai/Bi(上取整)次之后,再升级该技能将不会改变攻击力。

现在总计可以升级M次技能,可以任意选择升级的技能和次数。

最多可以提升多少点攻击力

这里答案也是具有单调性的,用的是尽量向右找的模板

#include <bits/stdc++.h>
using namespace std;
using ll=long long;
const int N=100010;
int a[N],b[N];
int n,m;
bool check(ll mid)
{
    ll res=0;
    for(int i=0;i<n;i++)
    {
        if(a[i]>=mid)
        res+=(a[i]-mid)/b[i]+1;
    }
    return res>=m;
}
int main()
{
    cin>>n>>m;
    for(int i=0;i<n;i++)
    {
        cin>>a[i]>>b[i];
    }
    int l=0,r=1e6;
    while(l<r)
    {
        ll mid=l+r+1>>1;
        if(check(mid)) l=mid;
        else r=mid-1;
    }
    ll res=0,cnt=0;;
    for(int i=0;i<n;i++)
    {
        if(a[i]>=r){
            int c=(a[i]-r)/b[i]+1;
            int end=a[i]-(c-1)*b[i];
            cnt+=c;
            res+=(ll)(a[i]+end)*c/2;
        }
    }
    cout<<res-(cnt-m)*r;
    return 0;
}

AcWing 789. 数的范围

这个题目代码比较简单,就不多写了

#include <bits/stdc++.h>
using namespace std;
const int N=1e6;
int a[N];
int n;
int q;
int main()
{
    cin>>n>>q;
    for(int i=0;i<n;i++)
    {
        cin>>a[i];
    }
    while(q--){
        int k;
        cin>>k;
        int l=0,r=n-1;
        while(l<r)
        {
            int mid=l+r>>1;
            if(a[mid]>=k) r=mid;
            else l=mid+1;
        }
        int ll=l;
        r=n-1;
        while(ll<r)
        {
            int mid=ll+r+1>>1;
            if(a[mid]<=k) ll=mid;
            else r=mid-1;
        }
        if(a[l]!=k){
            cout<<"-1 -1"<<endl;
            continue;
        }
        cout<<l<<" "<<ll<<endl;
    }
    return 0;
}

AcWing 102. 最佳牛围栏

先看看题目描述:

农场由N块地组成,每块地有一定数量的牛,数量不少于一,不超过2000,

要用围栏把连续的地围起来,使得围起来的区域内每块地包含的牛的数量的平均值达到最大

围起区域内至少需要包含 F 块地,其中 F 会在输入中给出

在给定条件下,计算围起区域内每块地包含的牛的数量的平均值可能的最大值是多少

也就是:给定一段数列,求平均数最大,长度不小于L的子段,也就是二分判定

我们可以进行前缀和运算,比如说我要算出[3,5]的子段和,那么我们只需要输出a[5]-a[3]

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
const double eps=1e-5;
int n,f;
double a[N],s[N];
double ans,min_val;
int check(double mid)
{
    for(int i=1;i<=n;i++) s[i]=a[i]-mid,s[i]+=s[i-1];
    ans=-1e8,min_val=1e8;
    for(int i=f;i<=n;i++)
    {
        min_val=min(min_val,s[i-f]);
        ans=max(ans,s[i]-min_val);
    }
    return ans<=0?0:1;
}
int main()
{
    ios::sync_with_stdio(false);
    cin>>n>>f;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
    }
    double l=-1e6,r=1e6;
    while(r-l>eps)
    {
        double mid=(l+r)/2;//注意浮点数二分
        if(check(mid)) l=mid;
        else r=mid;
    }
    cout<<(long long)(r*1000);
    return 0;
}

AcWing 1460. 我在哪?

额懒得写题目描述了

FarmerJohn在一条有彩色邮箱的路上迷路了,每个农场都沿路设有一个彩色的邮箱,所以约翰希望能够通过查看最近的几个邮箱的颜色来唯一确定他所在的位置。颜色一共有26种,用 A-Z 对应表示。现在FarmerJohn知道邮箱颜色的排列,他想知道在这条路中,他最少可以通过几个邮箱的颜色来确定他的位置。

其实就是要求字符串中长度为k的子串在字符串中只出现一次的k的最小值

<font style="color:rgba(0, 0, 0, 0.9);background-color:rgba(0, 0, 0, 0.03);">count</font>方法用于检查某个元素是否存在于集合中。

这里我们二分答案,l还是不能从0开始取。yysy,stl挺好用,就是下个题会超时

#include <bits/stdc++.h>
using namespace std;
string s;
unordered_set<string> S;
int n;
bool check(int mid)
{
    S.clear();
    for(int i=0;i+mid-1<n;i++)
    {
        string c=s.substr(i,mid);
        if(S.count(c)) return false;
        S.insert(c);
    }
    return true;
}
int main()
{
    cin>>n>>s;
    int l=1,r=n;
    while(l<r)
    {
        int mid=l+r>>1;
        if(check(mid)) r=mid;
        else l=mid+1;
    }
    cout<<l;
    return 0;
}

AcWing 1221. 四平方和

题意就是每个数都可以表示为四个数的平方和(包括零),对于一个给定的正整数,可能存在多种平方和的表示法。对所有的可能表示法按 a,b,c,d 升序排列,最后输出第一个表示法。

暴力很好想但是会超时。

#include <bits/stdc++.h>
using namespace std;
int main()
{
    int n;
    cin>>n;
    for(int a=0;a*a<=n;a++)
    {
        for(int b=a;b*b+a*a<=n;b++)
        {
            for(int c=b;c*c+b*b+a*a<=n;c++)
            {
                int t=n-c*c-b*b-a*a;
                int d=sqrt(t);
                if(d*d==t){
                    cout<<a<<" "<<b<<" "<<c<<" "<<d;
                    return 0;
                }
            }
        }
    }
    return 0;
}

哈希表

#include <bits/stdc++.h>
using namespace std;
int n;
const int N=5e6+5;
int C[N],D[N];
int main()
{
    cin>>n;
    memset(C,-1,sizeof C);
    for(int c=0;c*c<=n;c++)
    {
        for(int d=c;d*d+c*c<=n;d++)
        {
            int s=d*d+c*c;
            if(C[s]==-1)
            C[s]=c,D[s]=d;
        }
    }
    for(int a=0;a*a<=n;a++)
    {
        for(int b=a;b*b+a*a<=n;b++)
        {
            int s=n-a*a-b*b;
            if(C[s]!=-1)
            {
                cout<<a<<" "<<b<<" "<<C[s]<<" "<<D[s];
                return 0;
            }
        }
    }
    return 0;
}

额二分我也不会

结尾

做到这里其实也没感觉到怎么做二分,悟道很难啊(个人水平不足),难点就是分析二段性(即可以分为两个区间)和check函数

二分可以分成二分查找(二分搜索)和二分答案

1.二分查找就是对问题的搜索,可以降低时间复杂度。

这里默一下板子

/*
整数二分
*/
//尽量往左找
while(l<r)
{
    int mid=l+r>>1;
    if(check(mid)) r=mid;
    else l=mid+1;
}
//尽量往右找
while(l<r){
    int mid =(l+r+1)>>1;
    if(check(mid)) l=mid;
    else r=mid-1;
}
/*
浮点数二分
*/
while(r-l>1e-5)//需要一个精度保证
{
    double mid=(l+r)/2;
    if(check(mid)) l=mid;
    else r=mid;
}

2.二分答案是假定已知答案然后对题目验证,这种题目有明显的特征就是求..最大值的最小、求...最小值的最大

  • 求...最小值的最大 往右
  • 求..最大值的最小 往左
posted @ 2025-08-18 18:06  流云鹤=  阅读(9)  评论(1)    收藏  举报