其他分享
首页 > 其他分享> > 二分查找和二分答案

二分查找和二分答案

作者:互联网

二分查找

  什么是二分查找?

    举个栗子:

    问:有这样的一个数组:1,3,10,19,20,25,35,45,86,95,114;如何查找出一个能比17大的数的下标呢?

    是一个一个的去判断比较吗?

    还是写一个hash表来进行查找?

    .................

 

    可以看出前一个时间的复杂度是很高的,虽然hash算法的时间复杂度为O(1),但是它的空间复杂度升到了O(n)。有没有一种即可以让时间复杂度降低,又可以让空间           复杂度不达到O(n)。

    答案肯定是有的——那就是二分查找

    我们就对上面的例子,进行讨论:

    问:25比17大还是小?    大

      19比17大还是小?    大

       3 比17大还是小?    小    

    因此19就一个比17大的数。对于一个区间,一半,一半的半,一半的一半的一半的去查找,如果被查找的数要大于中间值,说明这个数在上半部分,L的值就向向上移,    否则就R向下移,就是二分查找的思想。因此在开始的时候对于L和R的取值这是和重要的。

 

   例题:

题目描述

输入 n(n\le10^6)n(n≤106) 个不超过 10^9109 的单调不减的(就是后面的数字不小于前面的数字)非负整数 a_1,a_2,\dots,a_{n}a1​,a2​,…,an​,然后进行 m(m\le10^5)m(m≤105) 次询问。对于每次询问,给出一个整数 q(q\le10^9)q(q≤109),要求输出这个数字在序列中的编号,如果没有找到的话输出 -1 。

输入格式

第一行 2 个整数 n 和 m,表示数字个数和询问次数。

第二行 n 个整数,表示这些待查询的数字。

第三行 m 个整数,表示询问这些数字的编号,从 1 开始编号。

输出格式

m 个整数表示答案。

输入输出样例

输入 #1
11 3
1 3 3 3 5 7 9 11 13 15 15
1 3 6
输出 #1
1 2 -1 

     

  由此可以看出,L的取值是取数组的开端的下标,R就是数组末尾的下标。使用我们刚刚的思路进行解题。

  代码如下:

  

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e6+10;
ll num[N],res[N];

int main()
{
    ll n,q;
    cin>>n>>q;
    for(int i=1;i<=n;i++)
        cin>>num[i];
    
    ll L=1,R=n,x,len=0;
    for(int i=0;i<q;i++)
    {
        cin>>x;
        L=1,R=n;
        while(L <= R)
        {
            ll mid=(L+R)/2;
            if(x>num[mid]) L=mid+1;
            else R=mid-1;
        }    
        if(num[L]==x)
            res[len++]=L;
        else res[len++]=-1;
    }
    
    for(int i=0;i<len;i++)
        printf("%lld ",res[i]);
    return 0;
} 

 

    要注意的是L和R前面的判断条件,只有这个L和R的判断条件弄清楚了,二分查找就差不多会了。

 

  二分答案

    二分答案的重点代码和二分查找是一样的,只不过二分答案要多出一个cheak()的判断条件。(下面会介绍什么是cheak())

    当我们碰到一些在最大值中找最小值,最小值中找最大值或者一些方程的求解时,就可以使用二分答案,实际上就是使用二分查找一步的一步的将答案查找出来,因此需要对于查找的两端进行判断了(下面会介绍如何对两端的进行取值)。其实对于这类的题目读懂题真重要,当你读懂题后你才知道这题其实也没有这么的难,(我这种蒟蒻,就觉得二分答案这类题的题目真的很难读懂),先声明两个点,我接下来的内容在对区间的两端,我都定义为L和R,对于中间值,称为K。

 

  最大值中找最小值:

 

  例题一、

  题目描述

     伐木工人米尔科需要砍倒M米长的木材。这是一个对米尔科来说很容易的工作,因为他有一个漂亮的新伐木机,可以像野火一样砍倒森林。不过,米尔科只被允许砍倒单行树木。

  米尔科的伐木机工作过程如下:米尔科设置一个高度参数H(米),伐木机升起一个巨大的锯片到高度H,并锯掉所有的树比H高的部分(当然,树木不高于H米的部分保持不变)。米尔科就行到树木被锯下的部分。

  例如,如果一行树的高度分别为20,15,10和17,米尔科把锯片升到15米的高度,切割后树木剩下的高度将是15,15,10和15,而米尔科将从第1棵树得到5米,从第4棵树得到2米,共得到7米木材。

  米尔科非常关注生态保护,所以他不会砍掉过多的木材。这正是他为什么尽可能高地设定伐木机锯片的原因。帮助米尔科找到伐木机锯片的最大的整数高度H,使得他能得到木材至少为M米。换句话说,如果再升高1米,则他将得          不到M米木材。

  输入格式

    第1行:2个整数N和M,N表示树木的数量(1<=N<=1000000),M表示需要的木材总长度(1<=M<=2000000000)

    第2行:N个整数表示每棵树的高度,值均不超过1000000000。所有木材长度之和大于M,因此必有解。

    输出格式

       第1行:1个整数,表示砍树的最高高度。

  输入输出样例

    输入 #1     5 20
   4 42 40 26 46
      输出 #1
    36

  读完题后,有找到什么是最小值,什么是最大值吗?

  这里的最小值是得到的木材和,最大值是尽可能抬高切割的高度。请问L和R的值要怎样取呢?

  分析:

    题目要求的是切割下的木材的和要是m,在现实生活中,要是切割下的木材的和为一固定值,是不是需要移动高度,每一移动一高度,用大于该高度的数减去高度,将所有剩下的数相加,如果相加的数大于m,我们是不是会把高度下降一点,然后再重复刚刚的步骤,如果是小于,就往上移,反复这般,是不是可以找到一个最优的高度使得正好切割下的木材的和为m。

   因此在这里的L是1,R是某一颗树的最大高度,K是m,cheak()用于返回每一高度剩下的木材和。

  代码如下:

 

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int INF=0x7f7f7f7f;
const int N=1e7+10;
ll num[N];

ll cheak(ll x,ll len)
{
    ll ans=0;
    for(int i=1;i<=len;i++)
    {
        if(num[i]>x)
        {
            ans+=(num[i]-x);
        }
    }
    return ans;
}

int main()
{
    std::ios::sync_with_stdio(0);
    cin.tie(0);
    
    
    ll m=-INF, n,sum;
    cin>>n>>sum;
    for(int i=1;i<=n;i++)
    {
        cin>>num[i];
        m=max(m,num[i]);
    }
    
    ll L=1,R=m,temp;
    while(L<=R)
    {
        ll mid=(L+R)>>1,temp=cheak(mid,n);
        if(temp>=sum) L=mid+1;
        else R=mid-1;
    }
    
    printf("%lld\n",R);
    return 0;
}

 

 

  可能你觉得上一题的最大值和最小值不太清楚。这里还有一题能够很清晰的表示出二分答案。

 

 

 

  样例二、

题目描述

对于给定的一个长度为N的正整数数列A1∼N​,现要将其分成 M(M≤N)段,并要求每段连续,且每段和的最大值最小。

关于最大值最小:

例如一数列 4 2 4 5 1 要分成 3 段。

将其如下分段:

[4 2][4 5][1]

第一段和为 6,第 22 段和为 9,第 3 段和为 1,和最大值为 9。

将其如下分段:

[4][2 4][5 1]

第一段和为 4,第 2 段和为 6,第 3 段和为 6,和最大值为 6。

并且无论如何分段,最大值不会小于 6。

所以可以得到要将数列 4 2 4 5 1 要分成 3 段,每段和的最大值最小为 6。

输入格式

第 11 行包含两个正整数 N,M。

第 22 行包含 N 个空格隔开的非负整数 Ai​,含义如题目所述。

输出格式

一个正整数,即每段和最大值最小为多少。

输入输出样例

输入 #1
5 3
4 2 4 5 1
输出 #1
6

说明/提示

对于20% 的数据,N≤10。

对于 40% 的数据,N≤1000。

对于100% 的数据,1≤N≤10^5,M≤N,Ai < 10^8, 答案不超过 10^9。

 

  读完题的你,有没有找到最大值是什么,最小值是什么?

  最大值是每一次分类的连续的区间的和,最小值是这些最大值中最小的哪一个。

  分析:

    在此之前,不知道你们是否做过这样的一题,在一数组中找出规定的连续子序列的和为sum的次数。如果没有做过的,先去做完这题,再来看这题,你会发现这题其实没有这么难。

    就如上一段所说的,使用cheak返回规定sum的次数。K就是m,如果sum的次数大于m,就说明这个sum的值太小了,要往上找,即sum的值要变大,使得每一段的和的次数变小;如果sum的次数小于m,就往下找。因此我们可以使数组的中最大的哪一个数为L,所有的数的和为R。

   

  代码如下:

  

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e6+10;
ll num[N],n,m;

ll cheak(ll x)
{
    ll cnt=0,sum=0;
    for(int i=0;i<n;i++)
    {
        if(sum+num[i]<=x) sum+=num[i];
        else sum=num[i],cnt++;
    }
    return cnt+1;
}

int main()
{
    ll L=0,R=0;
    scanf("%lld%lld",&n,&m);
    for(int i=0;i<n;i++)
    {
        scanf("%lld",&num[i]);
        R+=num[i];
        L=max(L,num[i]);
    }
        
    while(L<=R)
    {
        ll mid=(L+R)>>1;
        ll t=cheak(mid);
        if(t>m) L=mid+1;
        else R=mid-1;
    }
    
    printf("%lld\n",L);
    return 0;
} 

  这里有些细节问题,这里不作解释(麻麻说,要自己学会思考呢,啊哈哈哈,其实我是懒了,将就一下吧,但还是要学会自己思考的丫)

 

 

 

 

    最小值中找最大值:

  思路是和上面是一样的,但是求的不同,如果上面的看懂的,那么下面的就不用再看了。这里做出说明:我接下来的内容在对区间的两端,我都定义为L和R,对于中间值,称为K。

 

  样例一:

  

题目描述

木材厂有一些原木,现在想把这些木头切割成一些长度相同的小段木头(木头有可能有剩余),需要得到的小段的数目是给定的。当然,我们希望得到的小段木头越长越好,你的任务是计算能够得到的小段木头的最大长度。木头长度的单位是cm。原木的长度都是正整数,我们要求切割得到的小段木头的长度也是正整数。

例如有两根原木长度分别为11和21,要求切割成到等长的6段,很明显能切割出来的小段木头长度最长为5.

输入格式

第一行是两个正整数N和K(1 ≤ N ≤ 100000,1 ≤ K ≤ 100000000),N是原木的数目,K是需要得到的小段的数目。

接下来的N行,每行有一个1到100000000之间的正整数,表示一根原木的长度。

输出格式

能够切割得到的小段的最大长度。如果连1cm长的小段都切不出来,输出”0”。

输入输出样例

输入 #1
3 7
232
124
456
输出 #1
114

   看懂题目的你们,是否有找到最大值是什么?最小值是什么?

    最大值是每一段的最大值,最小值是题目给出的数目k

    分析:

        L为1,既当每一段是1的情况,R为这些小木块的最长的长度,K为题目给出的K,cheak()返回当每一段为这个数X是的次数。如果返回的数小于K,说明数x太大了,要小一些,既要往下找;如果是大于K,说明这个数x太小了,要往上找。

    代码如下:

 

 

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e6+10;
ll num[N],n,k;

ll cheak(ll x)
{
    ll a=0,b=0;
    for(int i=0;i<n;i++)
        a+=(num[i]/x);
    return a;
}

int main()
{
    ll R=0,L=1;
    scanf("%lld%lld",&n,&k);
    
    for(int i=0;i<n;i++)
    {
        scanf("%lld",&num[i]);
        R=max(R,num[i]);
    }
    
    while(L<=R)
    {
        ll mid=(L+R)>>1,t=cheak(mid);
        if(t<k) R=mid-1;
        else L=mid+1;
    }
    printf("%lld\n",R);
    return 0;
}

 

 

    可能你们觉得上面那一的最小值和最大值不太好看出,那么这题句非常的看得出要使用二分答案了。

 

 

    样例二、

题目背景

 

B市和T市之间有一条长长的高速公路,这条公路的某些地方设有路标,但是大家都感觉路标设得太少了,相邻两个路标之间往往隔着相当长的一段距离。为了便于研究这个问题,我们把公路上相邻路标的最大距离定义为该公路的“空旷指数”。

 

题目描述

 

现在政府决定在公路上增设一些路标,使得公路的“空旷指数”最小。他们请求你设计一个程序计算能达到的最小值是多少。请注意,公路的起点和终点保证已设有路标,公路的长度为整数,并且原有路标和新设路标都必须距起点整数个单位距离。

 

输入格式

 

第1行包括三个数L、N、K,分别表示公路的长度,原有路标的数量,以及最多可增设的路标数量。

第2行包括递增排列的N个整数,分别表示原有的N个路标的位置。路标的位置用距起点的距离表示,且一定位于区间[0,L]内。

 

输出格式

 

输出1行,包含一个整数,表示增设路标后能达到的最小“空旷指数”值。

 

输入输出样例

 

输入 #1
101 2 1
0 101
输出 #1
51

 

说明/提示

公路原来只在起点和终点处有两个路标,现在允许新增一个路标,应该把新路标设在距起点50或51个单位距离处,这样能达到最小的空旷指数51。

50%的数据中,2 ≤ N ≤100,0 ≤K ≤100

100%的数据中,2 ≤N ≤100000, 0 ≤K ≤100000

100%的数据中,0 < L ≤10000000

 

    看懂题目的你们,是否找到最大值和最小值?

    最小值是使两个路标的间距尽可能的小,最大值是这些最小值的最大值。

    分析:

       L为当两个路标间距是1 的情况。R为题目给出的路标距离的最大值,当两个路标的距离是x的时候,cheak()返回的是这些的路标的数量cnt,KK为题目给定的k。如果cnt大于k的时候,说明这个间距太小了,使得放入的路标大于了题目所给的k,因此要x的值要再大些,既往上找。如果cnt小于k的时候,说明这个间距太大了,使得放入的路标不足题目所给的k,因此要往下找。

    代码如下:

  

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e6+10;
ll num[N],l,n,k;

ll cheak(ll x)
{
    ll cnt=0;
    for(int i=0;i<n;i++)
    {
        if((num[i+1]-num[i])>x)
        {
            cnt+=(num[i+1]-num[i])/x;
            if((num[i+1]-num[i])%x==0) cnt--;
        } 
    }
    return cnt;
}

int main()
{
    ll  R=0,L=1;
    scanf("%lld%lld%lld",&l,&n,&k);
    for(int i=0;i<n;i++)
    {
        scanf("%lld",&num[i]);
        R=max(R,num[i]);
    }
    
    while(L<R)
    {
        ll mid=(L+R)>>1,t=cheak(mid);
        if(t<=k) R=mid;
        else L=mid+1;
    }
    printf("%lld\n",R);
    return 0;
}

 

 

  一些方程的求解:

  其实这个还是很好写的吧,就是公式不太好找。要是能找出公式,就能在一段的区间中筛选出答案了。

  

  样例:

  

题目描述

 

有形如:ax^3+bx^2+cx^1+dx^0=0这样的一个一元三次方程。给出该方程中各项的系数(a,b,c,d均为实数),并约定该方程存在三个不同实根(根的范围在-100−100之间),且根与根之差的绝对值\≥1。要求由小到大依次在同一行输出这三个实根(根与根之间留有空格),并精确到小数点后22位。

提示:记方程f(x)=0,若存在2个数x1​和x2​,且x1​<x2​,f(x1​)×f(x2​)<0,则在(x1​,x2​)之间一定有一个根。

 

输入格式

 

一行,4个实数A,B,C,D。

 

输出格式

 

一行,3个实根,并精确到小数点后2位。

 

输入输出样例

 

输入 #1
1 -5 -4 20
输出 #1
-2.00 2.00 5.00

  
分析:
  先用for找出存在两个x1,x2能使得f(x1​)×f(x2​)<0,如果有,就在x1和x2之前使用二分答案找出一个答案。

  代码如#include<bits/stdc++.h>
#define IO std::ios::sync_with_stdio(0),cin.tie(0)
using namespace std;
double a,b,c,d;

double f(double i)
{
    return a*i*i*i+b*i*i+c*i+d;
}

int main()
{
    IO ;
    cin>>a>>b>>c>>d;
    
    int cnt=0;
    for(double i=-100;i<=100;i++)
    {
        double x1=f(i);
        double x2=f(i+1);
        if(!x1)
        {
            printf("%.2lf ",i);
            cnt++;
        }
        if(x1*x2<0)
        {
            double L=i,R=i+1;
            while((R-L)>=0.0001)
            {
                double mid=(L+R)/2;
                if(f(mid)*f(R)<=0) L=mid;
                else R=mid;
            }
            printf("%.2lf ",R);
            cnt++;
        }
        if(cnt==3) break;
    }
    return 0;
}

  

 

2020-05-30

12:28:06

 

标签:二分,num,路标,ll,int,最小值,查找,答案,最大值
来源: https://www.cnblogs.com/duhengerhou/p/12991974.html