二分查找和二分答案

二分查找(整数)

查找最后一个<=5的数的下标

//查找最后一个<=5的数的下标
int find(int q)
{
    int l=0,r=n+1;//开区间
    while (l+1<r)//l+1=r时结束
    {
        int mid=l+r>>1;
        if(a[mid]<=q)l=mid;
        else r=mid;
    }
    return l;
}

查找最后一个>=5的数的下标

//查找最后一个>=5的数的下标
int find(int q)
{
    int l=0,r=n+1;//开区间
    while (l+1<r)//l+1=r时结束
    {
        int mid=l+r>>1;
        if(a[mid]>=q)r=mid;
        else l=mid;
    }
    return r;
}

1.指针跳跃了logn次
2.可行区的指针最后一定指向答案
3,把指针设置成开区间

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

二分查找(浮点数)

double find(double y)
{
    double l=-100,r=100;
    while(r-1>1e-5)
    {
        double mid=(l+r)/2;
        if(mid*mid*mid<=y)l=mid;
        else r=mid;
    }
    return l;
}

在这里插入图片描述

二分答案

二分查找与二分答案有何区别?
二分查找:在一个已知的有序数据集上进行二分地查找
二分答案:答案有一个区间,在这个区间中二分,直到找到最优答案

什么是二分答案?
答案属于一个区间,当这个区间很大时,暴力超时。但重要的是——这个区间是对题目中的某个量有单调性的,此时,我们就会二分答案。每一次二分会做一次判断,看是否对应的那个量达到了需要的大小。
判断:根据题意写个check函数,如果满足check,就放弃右半区间(或左半区间),如果不满足,就放弃左半区间(或右半区间)。一直往复,直至到最终的答案。

如何判断一个题是不是用二分答案做的呢?
1、答案在一个区间内(一般情况下,区间会很大,暴力超时)
2、直接搜索不好搜,但是容易判断一个答案可行不可行
3、该区间对题目具有单调性,即:在区间中的值越大或越小,题目中的某个量对应增加或减少。
此外,可能还会有一个典型的特征:求…最大值的最小 、 求…最小值的最大。
1、求…最大值的最小,我们二分答案(即二分最大值)的时候,判断条件满足后,尽量让答案往前来(即:让r=mid),对应模板1;
2、同样,求…最小值的最大时,我们二分答案(即二分最小值)的时候,判断条件满足后,尽量让答案往后走(即:让l=mid),对应模板2;

最大化答案

bool check(int x)
{
    
}
int find()
{
    int l=下界-1,r=上界+1;
    while(l+1<r)
    {
        int mid=l+r>>1;
        if(check(mid))l=mid;//最大化
        else r=mid;
    }
    return l;
}

最小化答案

bool check(int x)
{

}
int find()
{
    int l=下界-1,r=上界+1;
    while(l+1<r)
    {
        int mid=l+r>>1;
        if(check(mid))r=mid;//最小化
        else l=mid;
    }
    return r;
}

整数二分查找习题

P2249 【深基13.例1】查找

查找
第一次出现,最小化,>=q

//比赛中可以直接用万能头文件
#include<iostream>
#include<cstring>
#include<algorithm>
#include<map>
#include<vector>
#include<queue>
//开longlong防止爆掉int,int范围2e9,longlong范围9e18
#define int long long //(有超时风险)
//简写,如果要改动PII,直接该这个就行了,vector常用
#define PII pair<int,int>
//#define x first
//#define y second

using namespace std;

//需要改动数组直接改动N,M即可
const int N=1e8,M=1e3+10;

//常用的数组
int a[N],pre[N];
int n,m;

int find(int q) {
    int l = 0, r = n + 1;//开区间
    while (l + 1 < r)//l+1=r时结束
    {
        int mid = l + r >> 1;
        if (a[mid] >= q)r = mid;
        else l = mid;
    }
    return a[r]==q?r:-1;
}

signed main()
{
    //关掉流同步,cin变快,但是不能用scanf,可以用printf;
    std::ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    cin>>n>>m;
    for(int i=1;i<=n;i++)
        cin>>a[i];
    while(m--)
    {
        int b;
        cin>>b;
        int res=find(b);
        cout<<res<<" ";
    }
	return 0;
}

P1102 A-B 数对

P1102
法一双指针

//双指针
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N=2e5+10;
int n,c,a[N];

int main(){
  cin>>n>>c;
  for(int i=1; i<=n; i++) cin>>a[i];
  sort(a+1,a+1+n);
  
  long long ans=0;
  for(int k=1,i=1,j=1; k<=n; k++){ //i<=j
    while(i<=n && a[i]-a[k]<c) i++;  
    while(j<=n && a[j]-a[k]<=c) j++;
    ans+=j-i;
  }
  cout<<ans<<endl;
  return 0;
}
//k负责枚举每个数,i指针负责指向a[i]-a[k]=c的左端
//j负责指向右端+1
//ans维护这段数的个数,ans+=j-i

法二暴力枚举(有三个TLE)

//比赛中可以直接用万能头文件
#include<iostream>
#include<cstring>
#include<algorithm>
#include<map>
#include<vector>
#include<queue>
//开longlong防止爆掉int,int范围2e9,longlong范围9e18
#define int long long //(有超时风险)
//简写,如果要改动PII,直接该这个就行了,vector常用
#define PII pair<int,int>
//#define x first
//#define y second

using namespace std;

//需要改动数组直接改动N,M即可
const int N=200010,M=1e3+10;

//常用的数组
int n,C;
int q[N];

signed main()
{
    //关掉流同步,cin变快,但是不能用scanf,可以用printf;
    std::ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    cin>>n>>C;
    for(int i=1;i<=n;i++)
        cin>>q[i];
    int cnt=0;
    for(int a=1;a<=n;a++)
        for(int b=1;b<=n;b++)
            if(q[a]-q[b]==C)
                cnt++;
    cout<<cnt<<endl;

    return 0;
}

法三写两个二分

//比赛中可以直接用万能头文件
#include<iostream>
#include<cstring>
#include<algorithm>
#include<map>
#include<vector>
#include<queue>
//开longlong防止爆掉int,int范围2e9,longlong范围9e18
#define int long long //(有超时风险)
//简写,如果要改动PII,直接该这个就行了,vector常用
#define PII pair<int,int>
//#define x first
//#define y second

using namespace std;

//需要改动数组直接改动N,M即可
const int N=2e5+10,M=1e3+10;

//常用的数组
int a[N],pre[N];
int n,C;
int q[N];

int find1(int q[],int len,int x)
{
    int l=0,r=n+1;
    while (l+1<r)
    {
        int mid=l+r>>1;
        if(q[mid]<x)
        {
            l=mid;
        }
        else
        {
            r=mid;
        }
    }
    if(q[r]==x)return r;
    else return -1;
}

int find2(int q[],int len,int x)
{
    int l=0,r=n+1;
    while (l+1<r)
    {
        int mid=l+r>>1;
        if(q[mid]<=x)
        {
            l=mid;
        }
        else
        {
            r=mid;
        }
    }
    if(q[l]==x)return l;
    else return -1;
}

signed main()
{
    //关掉流同步,cin变快,但是不能用scanf,可以用printf;
    std::ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    cin>>n>>C;
    for(int i=1;i<=n;i++)
        cin>>q[i];
    sort(q+1,q+n+1);
    int cnt=0;
    for(int B=1;B<=n;B++)
    {
        int A=q[B]+C;
        int res1=find1(q,n,A);//数组名,长度,A
        if(res1==-1)
        {
            continue;
        }
        else
        {
            int res2=find2(q,n,A);//如果res!=-1,应该记录在哪里结束
            cnt+=res2-res1+1;
        }
    }
    cout<<cnt<<endl;

    return 0;
}

浮点数二分查找习题

P1024 [NOIP2001 提高组] 一元三次方程求解

P1024

//比赛中可以直接用万能头文件
#include<iostream>
#include<cstring>
#include<algorithm>
#include<map>
#include<vector>
#include<queue>

//简写,如果要改动PII,直接该这个就行了,vector常用
#define PII pair<int,int>
//#define x first
//#define y second

using namespace std;

//需要改动数组直接改动N,M即可
const int N=2e5+10,M=1e3+10;

//常用的数组

double a,b,c,d;
double fun(double x)
{
    return a*x*x*x+b*x*x+c*x+d;
}
double find(double l,double r)
{
    while (r-l>0.0001)
    {
        double mid=(l+r)/2;
        if(fun(mid)*fun(r)<0)l=mid;//最大化
        else r=mid;
    }
    return l;
}
int main()
{
    scanf("%lf%lf%lf%lf",&a,&b,&c,&d);
    for(int i=-100;i<100;i++)
    {
        double y1=fun(i),y2=fun(i+1);//把区间一段一段的判断,左闭右开
        if(!y1)printf("%.2lf ",1.0*i);//特判一下左边界
        if(y1*y2<0)printf("%.2lf ",find(i,i+1));//判断一下左边界到右边界之间存不存在根
    }

    return 0;
}

二分答案习题

P2440 木材加工

P2240

//比赛中可以直接用万能头文件
#include<iostream>
#include<cstring>
#include<algorithm>
#include<map>
#include<vector>
#include<queue>
//开longlong防止爆掉int,int范围2e9,longlong范围9e18
#define int long long //(有超时风险)
//简写,如果要改动PII,直接该这个就行了,vector常用
#define PII pair<int,int>
//#define x first
//#define y second

using namespace std;

//需要改动数组直接改动N,M即可
const int N=2e5+10,M=1e3+10;

//常用的数组
int n,k,a[N];
//找一个数给每个a[i]除,看结果加起来是不是大于k
bool check(int x)
{
    int y=0;//段数
    for(int i=1;i<=n;i++)
        y+=a[i]/x;
    return y>=k;//x小y大
}
//把段的长度二分,从而找到x的最大值
int find()
{
    int l=0,r=1e8+1;
    while (l+1<r)
    {
        int mid=l+r>>1;
        if(check(mid))l=mid;//最大化
        else r=mid;
    }
    return l;
}
signed main()
{
    //关掉流同步,cin变快,但是不能用scanf,可以用printf;
    std::ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    cin>>n>>k;
    for(int i=1;i<=n;i++)
        cin>>a[i];
    cout<<find()<<endl;
    return 0;
}
//x是最大的长度,y是段数

在这里插入图片描述

P2678 [NOIP2015 提高组] 跳石头

P2678
在这里插入图片描述

//比赛中可以直接用万能头文件
#include<iostream>
#include<cstring>
#include<algorithm>
#include<map>
#include<vector>
#include<queue>
//开longlong防止爆掉int,int范围2e9,longlong范围9e18
#define int long long //(有超时风险)
//简写,如果要改动PII,直接该这个就行了,vector常用
#define PII pair<int,int>
//#define x first
//#define y second

using namespace std;

//需要改动数组直接改动N,M即可
const int N=2e6+10;

//常用的数组
int L,n,M,a[N];

bool check(int x)
{
    int last=0,cnt=0;//last当前站的石头的编号,cnt移走的石头的块数
    for(int i=1;i<=n+1;i++)
        if(a[i]-a[last]<x)cnt++;//贪心,如果距离小于x,就把石头移走
    else
        last=i;//否则就跳到这块石头上
    return cnt<=M;//x小,cnt小
}

int find()
{
    int l=0,r=1e9+1;
    while (l+1<r)
    {
        int mid=l+r>>1;
        if(check(mid))l=mid;//最大化
        else r=mid;
    }
    return l;
}

signed main()
{
    //关掉流同步,cin变快,但是不能用scanf,可以用printf;
    std::ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    cin>>L>>n>>M;
    for(int i=1;i<=n;i++)
        cin>>a[i];
    a[n+1]=L;//右边界
    cout<<find()<<endl;

    return 0;
}
//x是最短跳跃距离的最大值
//y是至多从起点和终点之间移走M块岩石,y<=M
//二分枚举x,移走一定的石头,剩余的石头之间的距离>=x
//站在一块石头上,看下一块,间距<x就移走,>=x就保留
//特判倒数第二块岩石到最后一块岩石的距离,如果小于x,就移走倒数第二块岩石,把终点的岩石的编号改成n+1

P1314 [NOIP2011 提高组] 聪明的质监员

P1314

#include<iostream>
#include<cstring>
#include<algorithm>

using namespace std;
typedef long long LL;

//需要改动数组直接改动N,M即可
const int N=200010;

//常用的数组
int n,m,w[N],v[N],l[N],r[N];
LL s,sn[N],sv[N],ans=1e18;


bool check(int W)
{
    //前缀和
    memset(sn,0, sizeof(sn));
    memset(sv,0, sizeof(sv));
    for(int i=1;i<=n;i++)
    {
        if(w[i]>=W)sn[i]=sn[i-1]+1,sv[i]=sv[i-1]+v[i];//sn是个数,sv是价值
        else sn[i]=sn[i-1],sv[i]=sv[i-1];//不符合就不用加上
    }

    //区间和
    LL y=0;
    for(int i=1;i<=m;i++)
        y+=(sn[r[i]]-sn[l[i]-1])*(sv[r[i]]-sv[l[i]-1]);
    ans=min(ans,llabs(y-s));//最优解
    return y<=s;//W大,y小,看第32行
}

LL find()
{
    int l=0,r=1e6+1;
    while (l+1<r)
    {
        int mid=l+r>>1;
        if(check(mid))r=mid;//最小化
        else l=mid;
    }
    return ans;
}

int main(){
    scanf("%d %d %lld",&n,&m,&s);
    for(int i=1;i<=n;i++)
        scanf("%d%d",&w[i],&v[i]);
    for(int i=1;i<=m;i++)
        scanf("%d%d",&l[i],&r[i]);
    printf("%lld",find());
    return 0;
}
//调整参数w,作为变量x
//y为各个区间的检验值之和
//yi=重量*重量*价值
//多次算区间和,需要先处理下前缀和
//计算出y后,让y和标准值比较,求最优解,没有说y要小于s还是大于s,假设一种情况就可以
//假设y<=s,x在右侧,可行区在右边,用最小化的板子

为什么用memset
这段代码使用 memset 函数来清空数组 sn 和 sv,确保它们在每次调用 check 函数时都是空数组的状态。这是因为这两个数组在 check 函数中用于计算前缀和 sn 和 sv,以及用于保存每个区间的数量和价值之和。在每次调用 check 函数之前,需要确保这两个数组是干净的,即里面的值都是初始化的零,以避免之前的计算结果对当前计算的结果产生影响。
所以,通过 memset 函数将数组初始化为零,可以确保每次计算前都是从一个干净的状态开始,避免了可能的错误结果。

P1083 [NOIP2012 提高组] 借教室

P1083

#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;

const int N = 1000010;
int n,m;
int r[N],d[N],s[N],t[N];
long long num[N]; //每天教室需求数

bool check(int x){
    memset(num,0,sizeof num);
    for(int i=1;i<=x;i++){//枚举订单数
        num[s[i]]+=d[i];
        num[t[i]+1]-=d[i]; //差分
    }
    for(int i=1;i<=n;i++){//枚举天数
        num[i]+=num[i-1]; //前缀和
        if(num[i]>r[i])return false;//是否型,当订单超过某个数量,那就失败了,后面的订单也失败了
    }
    return true; //x小,true
}
int find(){
    int l=0,r=m+1;
    while(l+1<r){
        int mid=l+r>>1;
        if(check(mid)) l=mid; //最大化
        else r=mid;
    }
    return l;
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%d",&r[i]);
    for(int i=1;i<=m;i++)
        scanf("%d%d%d",&d[i],&s[i],&t[i]);
    if(check(m)){puts("0");return 0;}
    printf("-1\n%d",find()+1);//匹配失败的订单
    return 0;
}
//差分:开始的位置加上2,段尾之后的位置加上2,不用都加上,这样可以降低时间复杂度
//先差分然后求前缀和,前1,2,3,4,5```n个位置的和
//把时间复杂度从O(mn)降低到O(m+n)
//返回true,就让指针右移,最大化,如果不满足输出l+1,因为l是最后一个满足的,l+1不满足

在这里插入图片描述
在这里插入图片描述

P1902 刺杀大使

P1902

//比赛中可以直接用万能头文件
#include<iostream>
#include<cstring>
#include<algorithm>
#include<map>
#include<vector>
#include<queue>
//开longlong防止爆掉int,int范围2e9,longlong范围9e18

//简写,如果要改动PII,直接该这个就行了,vector常用
#define PII pair<int,int>
//#define x first
//#define y second

using namespace std;

//需要改动数组直接改动N,M即可
const int N=2e3+10,M=1e3+10;
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
int n,m,p[N][N];
bool vis[N][N];

bool dfs(int x,int y,int P)
{
    if(x==n)return true;
    vis[x][y]= true;//标记
    for(int i=0;i<4;i++)
    {
        int a=x+dx[i],b=y+dy[i];
        if(a>=1&&a<=n&&b>=1&&b<=m&&!vis[a][b]&&p[a][b]<=P)
            if(dfs(a,b,P))return true;//P大,易true
    }
    return false;
}

int find()
{
    int l=-1,r=1001;
    while (l+1<r)
    {
        int mid=l+r>>1;
        memset(vis,0,sizeof vis);
        if(dfs(1,1,mid))r=mid;//最小化
        else l=mid;
    }
    return r;
}

signed main(){
    scanf("%d%d",&n,&m);
    for(int i=1; i<=n; i++)
        for(int j=1; j<=m; j++)
            scanf("%d",&p[i][j]);
    printf("%d",find());
    return 0;
}
//把伤害值作为x,y是走通1,走不通0,是否型
//能走通返回true,也就是右边.右边怎么样靠近答案?最小化,左移

在这里插入图片描述

P1873 [COCI 2011/2012 #5] EKO / 砍树

P1873
法一常规做法,会TLE,能过4-%

//比赛中可以直接用万能头文件
#include<iostream>
#include<cstring>
#include<algorithm>
#include<map>
#include<vector>
#include<queue>

//简写,如果要改动PII,直接该这个就行了,vector常用
#define PII pair<int,int>
//#define x first
//#define y second

using namespace std;

//需要改动数组直接改动N,M即可
const int N=2e6+10,M=1e3+10;
int n,m;
int q[N];


signed main()
{
    //关掉流同步,cin变快,但是不能用scanf,可以用printf;
    std::ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    cin>>n>>m;
    int hightest=0;
    for(int i=1;i<=n;i++)
    {
        cin>>q[i];
        hightest=max(q[i],hightest);
    }

    int res=0;
    int sum=0;
    for(int i=1;i<=hightest;i++)
    {
        sum=0;
        for(int j=1;j<=n;j++)
        {
            sum+=max(0,q[j]-i);
            if(sum>=m)
            {
                res=max(res,i);
                continue;
            }
        }
    }
    cout<<res<<endl;
    return 0;
}

法二最小值最大,答案在蓝色区域,绿色是最优解(最大化答案)

//比赛中可以直接用万能头文件
#include<iostream>
#include<cstring>
#include<algorithm>
#include<map>
#include<vector>
#include<queue>

//简写,如果要改动PII,直接该这个就行了,vector常用
#define PII pair<int,int>
//#define x first
//#define y second

using namespace std;

//需要改动数组直接改动N,M即可
const int N=2e6+10,M=1e3+10;
int n,m;
int q[N];

bool check(int x)
{
    int sum=0;
    for(int i=1;i<=n;i++)
    {
        sum+=max(0,q[i]-x);
        if(sum>=m)return true;
    }
    return false;
}

signed main()
{
    //关掉流同步,cin变快,但是不能用scanf,可以用printf;
    std::ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    cin>>n>>m;
    int hightest=0;
    for(int i=1;i<=n;i++)
    {
        cin>>q[i];
        hightest=max(q[i],hightest);
    }

    int res=0;//记录daan
    int sum=0;//记录当前得到的树的总高度

    int l=0,r=hightest;
    while (l+1<r)
    {
        int mid=l+r>>1;
        if(check(mid))l=mid;
        else r=mid;
    }
    if(check(r))//题目有可能卡你,答案在分界线的右边第一个,需要check一下
        cout<<r<<endl;
    else
        cout<<l<<endl;//大部分情况不会卡你

    return 0;
}

在这里插入图片描述

总结

在这里插入图片描述
画出图像,确定可行区,在左边就是最大化答案l=mid
在右边就是最小化答案r=mid

  • 12
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值