『二分查找和二分答案』

版权声明:随你转载,你开心就好(记得评论和注明出处哦) https://blog.csdn.net/Prasnip_/article/details/80991571

<前言>
分治算法的一类,也是很重要的一类。


<更新提示>

<第一次更新>


<正文>

二分查找

引入
问题

给定n个有序的整数,问整数x是否这n个数里。

分析

使用暴力思想,当然是一个一个查找判断。但如果要查找m个数呢?如果n*m>108呢?我们就必须使用一种新算法了——二分查找。

因为n个数是有序的,所以,我们让第mid=n/2个数与x比较,如果mid=x,那就找到了,如果mid < x,那么x就一定在区间[mid,n]中,反之,则x在[1,mid]中。这样,每一次比较,就可以缩小1/2的范围,我们继续这样查找,就能以log2n的效率找到x或判定x不存在。我们称之为二分查找。

代码实现
inline int find(int x)
{
    int left=1,right=n;
    while(left+1<right)
    {
        int mid=(left+right)>>1;
        if(a[mid]==x)return mid;
        if(a[mid]<x)left=mid;
        if(a[mid]>x)right=mid;
    } 
    if(a[left]==x)return left;
    if(a[right]==x)return right;
    return -1;
}
小结

二分查找是一种分治的策略,可以用于一个单调的大范围内的查找,效率极高。作为枚举,二分查找也可以找到满足条件的最大最小值。

例题
1

A-B Problem
自定义测试
题目描述
给出一串数以及一个数字C,要求计算出所有A-B=C的数对的个数。(不同位置的数字一样的数对算不同的数对)
输入格式
第一行包括2个非负整数N和C,中间用空格隔开。 第二行有N个整数,中间用空格隔开,作为要求处理的那串数。
输出格式
输出一行,表示该串数中包含的所有满足A-B=C的数对的个数。
样例数据
input
4 1
1 1 2 3
output
3
数据规模与约定
对于100%的数据,N <= 200000。
所有输入数据都在int范围内。
时间限制:
1s
1s
空间限制:
256MB
256MB

分析:

显然,这是一道二分查找的模板题,怎么查找呢?这回不是精确查找了。对于数列中的每一个数k[i],我们需要找到序列中大于k[i]-c的第一个位置,在找到大于等于k[i]-c的第一个位置,若两个位置相同,则说明没有恰为k[i]-c的数,如果两个位置不同,则说明恰有一个数为k[i]-c,由于数k[i]是已知存在的数,那么就说明存在这这么一个数对,统计答案加一。那么怎么实现查找呢,当然是二分查找。我们可以手动实现,当然,c++内置的函数库中也为我们准备了函数。
upper_bound(a,a+n,k)意为在数组a的区间[a,a+n)内二分查找数k,返回值为第一个大于k的位置。
lower_bound(a,a+n,k)与upper_bound相似,但返回值是第一个大于等于k的位置。
这样,我们利用这两个二分函数就能实现快速查找了。
代码实现如下:

#include<bits/stdc++.h>
using namespace std;
int k[1<<21]={};
int main()
{
    int n,c;
    cin>>n>>c;
    for(int i=1;i<=n;i++)
    {
        cin>>k[i];
    }
    sort(k+1,k+n+1);
    int ans=0;
    for(int i=1;i<=n;i++)
    {
        ans+=upper_bound(k+1,k+n+1,k[i]-c)-lower_bound(k+1,k+n+1,k[i]-c);
    }
    cout<<ans<<endl;
    return 0;
}
2

mnotes
统计
描述
提交
自定义测试
题目描述
FJ准备教他的奶牛们弹一首歌. 这首歌由N (1 <= N <= 50,000)个音阶组成,第i个音阶要敲击 B_i (1<= B_i <= 10,000) 次。奶牛从第0时刻开始弹, 因此他从0时刻到B_1 - 1 时刻都是敲第1个音阶,然后他从 B_1 时刻到B_1 + B_2 - 1时刻敲第2个音阶,从B_1 + B_2时刻到B_1 + B_2 + B_3- 1时刻敲第3个音阶…现在有Q (1 <= Q <= 50,000) 个问题,在时间段区间 [T, T+1)内,奶牛敲的是哪个音阶? 其中 T_i (0 <= T_i <= 整首歌的总时刻).
看下面的一首歌,第1个音阶持续2个单位时间, 第2个音阶持续1个单位时间,第3个音阶持续3个单位时间:
这里写图片描述
以下是一些询问和回答:

询问 回答的音阶
2 2
3 3
4 3
0 1
1 1

输入格式
第 1 行:两个整数: N 、Q。
第 2..N+1行: 第i+1行只有一个整数: B_i
第N+2..N+Q+1行: 第N+i+1行只有一个整数: T_i
输出格式
一行,一个整数,表示答案。
样例数据
input
3 5
2
1
3
2
3
4
0
1
output
2
3
3
1
1
数据规模与约定
时间限制:
1s
1s
空间限制:
256MB
256MB

分析

对与数据的输入,我们采用类似前缀和统计的方式将其转化为有序序列,然后对于询问,直接二分查找即可。
代码实现如下:

#include<bits/stdc++.h>
using namespace std;
int n,q,timee[50080]={},temp,l,r,mid; 
int main()
{
    cin>>n>>q;
    for(int i=1;i<=n;i++)
    {
        cin>>timee[i];
        timee[i]+=timee[i-1];
    }
    for(int i=1;i<=q;i++)
    {
        cin>>temp;
        l=1,r=n;
        while(l+1<r)
        {
            mid=(l+r)/2;
            if(timee[mid]>temp)r=mid;
            else l=mid;
        } 
        if(timee[l]>temp)cout<<l<<endl;
        else cout<<r<<endl;
    }
}

二分答案

引入
问题

使得x^x达到或超过n位数字的最小正整数x是多少?n<=2000000000

分析

对与这种较难求解的问题,我们很难想出较好的解决策略。但是,我们至少知道答案一定在1与2000000000之间,能否转换二分查找的思想,对答案进行二分查找呢?当然是可以的,但在二分查找中有比较,在二分答案中,我们也需要有比较,我们用检查函数来实现。如果检查出的是一个合法的解,那么我们尝试找更优的解,如果检查出的是不合法的解,我们尝试找合法的解,这就是二分答案的思想。在这题中,我们设计的检查函数就是x^x的数位是否超过n位,合法返回真,不合法返回假。再二分求解即可。

代码实现
#include<bits/stdc++.h>
using namespace std;
int o;
inline bool check(int k)
{
    long long p=k;
    return p*log10(1.0*k)+1>=o;//判断数位
}
inline int medium()
{
    int l=0,r=2000000000;
    while(l+1<r)
    {
        int mid=(l+r)/2;
        if(check(mid))r=mid;//这是一个合法的解,尝试缩小范围,找更优的解
        else l=mid;//解不合法,找合法的解
    }
    if(check(l))return l;
    else return r;
}
int main()
{
    cin>>o;
    cout<<medium()<<endl;
    return 0;
}
小结

二分查找是一种思维转换策略,我们可以把一个很难的求解性问题转换为较为简单的判断性问题。
对于二分答案的代码实现我们一般都要根据题目要求进行设计。但有这样的模板:while循环查找时,判断条件为l+1 < r,缩小范围时使l,r等于mid,这样我们最后找到的就是相邻的两个l,r,再以是否合法为第一条件,题目需求为第二条件,判断输出即可。
二分答案题有很明显的特征:求让最大值最小或最小值最大。

例题
1

路由器安置
题目描述
一条街道安装WIFI,需要放置M个路由器。整条街道上一共有N户居民,分布在一条直线上,每一户居民必须被至少一台路由器覆盖到。现在的问题是所有路由器的覆盖半径是一样的,我们希望用覆盖半径尽可能小的路由器来完成任务,因为这样可以节省成本。
输入格式
输入文件第一行包含两个整数M和N,以下N行每行一个整数Hi表示该户居民在街道上相对于某个点的坐标。
输出格式
输出文件仅包含一个数,表示最小的覆盖半径,保留一位小数。
样例数据
input
2 3
1
3
10
output
1.0
数据规模与约定
时间限制:
1s
1s
空间限制:
256MB
256MB
注释
对于60%的数据,有1 ≤ N, M ≤ 100,-1000 ≤ Hi ≤ 1000; 对于100%的数据,有1 ≤ N, M ≤ 100000,-10000000 ≤ Hi ≤ 10000000。

分析

看到巨大的数据,我们考虑二分答案。其中,路由器直径最小为1,最多为距离最远的两户居民的距离加一。如何设计判断函数呢?显然,利用贪心策略即可。我们将每一个路由器放置在能覆盖到某户居民的最右端,让它尽可能靠右,尝试能否覆盖到另一户居民。然后再查找到下一户居民即可。以使用的路由器个数判断方案是否合法。这里,检查函数中又用到了二分查找,我们直接调用upper_bound函数即可,减轻了代码量。
代码实现如下:

#include<bits/stdc++.h>
using namespace std;
int m,n;
int dis[180000]={};
inline int read()
{
    int w=0,x=0;char ch;
    while(!isdigit(ch)){w|=ch=='-';ch=getchar();}
    while(isdigit(ch)){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();}
    return w?-x:x;
}
inline bool check(int d)
{
    int num=1,tot=1;
    while((upper_bound(dis+1,dis+n+1,dis[num]+d)-dis)<=n)
    {
        tot++;
        if(tot>m)return false;
        num=(upper_bound(dis+1,dis+n+1,dis[num]+d)-dis);
    }
    return true;
}
int main()
{
    m=read(),n=read();
    for(int i=1;i<=n;i++)dis[i]=read();
    sort(dis+1,dis+n+1);
    int l=1,r=dis[n]-dis[1]+1;
    while(l<r)
    {
        int mid=(l+r)/2;
        if(check(mid))r=mid;
        else l=mid+1;
    }
    printf("%.1lf",(double)l/2.0);
    return 0;
} 
2

擦护栏
题目描述
Jzyz的所有学生获得了一个大扫除的机会——去大街擦护栏。 Jz市大街被分成了M段,每段的长度不一定相同。Jzyz一共有N名学生参加劳动,这些学生将分成M组来完成这项工作,因为长度不同,分配的任务量肯定不同,现在,负责这次大扫除的老师想知道,擦护栏长度最多的同学最少必须擦多长的护栏,这样才能保证尽可能的公平。当然,可以有学生当拉拉队,不用擦护栏,但是每段护栏必须要擦干净。 比如:有5名学生,2段护栏,第一段长度为7,第二段长度为4.可以让3个人负责擦长度为7的,2个人负责长度为4的,那么擦第一段的某个人必须要擦长度为3 的护栏,而其他的人擦长度为2 的护栏。这样就有1位同学必须擦长度为3的护栏。
输入格式
第一行:两个整数N和M(1 ≤ N ≤ 10^9) (1 ≤ M ≤ 300 000, M ≤ N)
接下来M行,每行一个整数,表示每段护栏的长度,保证护栏的长度在[1,10^9] 之间。
输出格式
一个整数,如题目描述,任务量最重的同学擦护栏长度的最小值。
样例数据
input
7 5
7
1
7
4
4
output
4
数据规模与约定
【数据范围】 20% 数据保证 N<=30 M<=20 40% 数据保证 N<=10000 M<=1000 100%数据保证 如题目描述
时间限制:
1s
1s
空间限制:
256MB

分析

看到最长的最短,自然想到二分答案。我们对擦护栏的最长长度继续二分,其中,该长度最长为最长的护栏长度,最短为1。判断时,我们检查以该长度为最长长度需要的人数,判断人数是否足够,即为是否合法。
代码实现如下:

#include<bits/stdc++.h>
using namespace std;
int n,m,len[300080]={},s=0;
inline bool check(int k)
{
    int tot=0;
    for(int i=1;i<=m;i++)
    {
        tot+=len[i]/k;
        if(len[i]%k!=0)tot++;
        if(tot>n)return false; 
    }
    return true;
}
int main()
{
    cin>>n>>m;
    for(int i=1;i<=m;i++)cin>>len[i],s=max(s,len[i]);
    int l=1,r=s;
    while(l+1<r)
    {
        int mid=(l+r)/2;
        if(check(mid))r=mid;
        else l=mid;
    } 
    if(check(l))cout<<l<<endl;
    else cout<<r<<endl;
    return 0;
} 
3

烤鱼
题目描述
小x掉到了一个jzyz的窨井里,这口井很深,没有人能听到小x的呼救,悲催的小x必须要等D天后,学校确认他失踪才会大规模寻找他,而这难熬的D天将是小x这一生最难过的D天。 不过幸运的是小x在井里得到了N (1 <= N <= 50,000) 条鱼,编号1..N.他计划在接下来的D (1 <= D <= 50,000)天按鱼的编号从小到大逐天吃,当然,小x可以一天连续吃多条鱼,也可以不吃。   为了不浪费,小x要求鱼必须吃完。。 对于第i条鱼,小x的能量值会增加Hi(1 <= Hi <= 1,000,000)。小x会在白天吃鱼,一旦吃饱,他就会一觉睡到第二天。第二天醒来,她的能量值会变成前一天的能量值的一半(向下取整),小x的能量值是可以累加的。 小x比较注意维护每天能量的平衡,要刻意避免能量的大起大落,于是现在小x想知道,如何安排吃鱼,才能保证能量值最小的那个晚上(假设是第X个晚上),第X个晚上的能量值最大。 例如:有5条鱼,能量值分别是: (10, 40, 13, 22, 7). 小x按以下方式吃鱼,将会得到最优值:
·第几天
·醒来时能量值
·吃了鱼后能量值
·晚上睡觉能量值·
1
0
10+40 
50
2
25
—  
25 
                    
3
12
13  
25
4
12
22  
34
                    
5
17
7   
24
 
可以看出,第1天吃了鱼1、鱼2,第2天不吃鱼,第3天吃了鱼3,第4天吃了鱼4,第5天吃了鱼5。 那么在这5个晚上,能量值最低的是第5个晚上,能量值是24,所以输出24。然后你还要输出第I (1<=i<=N)条鱼是小x第几天吃掉的。
输入格式
第1行:两个整数: N 和 D 第2..N+1行: 每行一个整数Hi。
输出格式
第1行: 一个整数, 在这D个晚上里,能量值最小的那个晚上(假设是第X个晚上),第X个晚上的能量值最大可以是多少? 第2…..N+1行: 每行一个整数,第 i 行表示第i条鱼是第几天被吃的。
样例数据
input
5 5
10
40
13
22
7
output
24
1
1
3
4
5
数据规模与约定
40% 保证 N,D<=200
100%数据如题目描述
时间限制:
1s
1s
空间限制:
256MB
256MB

分析

最小的最大,依旧二分。我们先看第一问,利用二分答案求出。判断函数依据题意模拟即可,判断的条件是当营养值最小的那个晚上最大为mid时,n条鱼是否够吃,如果够吃则该数值合法,反之则不合法。对于第二问,我们只需再重复模拟一遍类似检查函数的过程,将方案输出即可。注意,这道题的坑点在输出方案时,如果到了最后一天了还有多余的鱼,需要把那些鱼判断为全是最后一天吃的。即特判最后一天是否又剩下的鱼即可。
代码实现如下:

#include<bits/stdc++.h>
using namespace std; 
long long n,d,h[50080]={},s=0,ans;
inline long long check(long long k)
{
    long long ate=0,now=0;
    for(long long i=1;i<=d;i++)
    {
        now/=2;
        while(now<k)
        {
            now+=h[++ate];
            if(ate>n)return false;
        }
    }
    return true;
}
int main()
{
    cin>>n>>d;
    for(long long i=1;i<=n;i++)cin>>h[i],s+=h[i];
    long long l=1,r=s;
    while(l+1<r)
    {
        long long mid=(l+r)>>1;
        if(check(mid))l=mid;
        else r=mid;
    }
    if(check(r))ans=r,cout<<r<<endl;
    else ans=l,cout<<l<<endl;
    long long now=0,ate=0;
    for(long long i=1;i<=d;i++)
    {
        now/=2;
        while(now<ans)
        {
            now+=h[++ate];
            cout<<i<<endl;
        }
 }
    if(ate<n)
    {
        for(int i=1;i<=n-ate;i++)
        {
            cout<<d<<endl;
        }
    }
}
4

跳石头
题目描述
一年一度的“跳石头”比赛又要开始了!
这项比赛将在一条笔直的河道中进行,河道中分布着一些巨大岩石。组委会已经选择好了两块岩石作为比赛起点和终点。在起点和终点之间,有 N 块岩石(不含起点和终 点的岩石)。在比赛过程中,选手们将从起点出发,每一步跳向相邻的岩石,直至到达 终点。
为了提高比赛难度,组委会计划移走一些岩石,使得选手们在比赛过程中的最短跳 跃距离尽可能长。由于预算限制,组委会至多从起点和终点之间移走 M 块岩石(不能 移走起点和终点的岩石)。
输入格式
输入文件名为 stone.in。
输入文件第一行包含三个整数 L,N,M,分别表示起点到终点的距离,起点和终 点之间的岩石数,以及组委会至多移走的岩石数。
接下来 N 行,每行一个整数,第 i 行的整数 Di(0 < Di < L)表示第 i 块岩石与 起点的距离。这些岩石按与起点距离从小到大的顺序给出,且不会有两个岩石出现在同 一个位置。
输出格式
输出文件名为 stone.out。 输出文件只包含一个整数,即最短跳跃距离的最大值。
样例数据
input
25 5 2
2
11
14
17
21
output
4
数据规模与约定
输入输出样例 1 说明:将与起点距离为 2 和 14 的两个岩石移走后,最短的跳跃距离为 4(从与起点距离 17 的岩石跳到距离 21 的岩石,或者从距离 21 的岩石跳到终点)。
另:对于 20%的数据,0 ≤ M ≤ N ≤ 10。 对于50%的数据,0 ≤ M ≤ N ≤ 100。
对于 100%的数据,0 ≤ M ≤ N ≤ 50,000,1 ≤ L ≤ 1,000,000,000。
时间限制:
1s
1s
空间限制:
256MB

分析

依旧二分答案,其距离最短为0,最长为l,进行二分,判断函数根据最短距离最长为mid时所需移去的石头数是否在m以内进行判断,在继续对答案进行二分查找即可。
代码实现如下:

#include<iostream>
using namespace std;
int l,n,m,a[50010],z,y;
int check(int mid)
{
    int sum=0,k=0;
    for(int i=0;i<n;i++)
    if(a[i]-k<mid)
    {

        sum++;
    }
    else k=a[i];
    if(sum>m)return 0;
    else return 1;

}
int main()
{
    cin>>l>>n>>m;
    for(int i=0;i<n;i++)cin>>a[i];
    z=0;y=l;
    while(z<=y)
    {
        int mid=(z+y)/2;
        if(!check(mid))y=mid-1;
        else z=mid+1;
    }
    cout<<y;
}
5

聪明的质监员
题目描述
小 T 是一名质量监督员,最近负责检验一批矿产的质量。这批矿产共有 n 个矿石,从 1 到 n 逐一编号,每个矿石都有自己的重量 wi 以及价值 vi。检验矿产的流程是:
1、给定 m个区间[Li,Ri];
2、选出一个参数 W;
3、对于一个区间[Li,Ri],计算矿石在这个区间上的检验值 Yi :

Y i = j 1 j v j

j∈[Li ,Ri ]且wj ≥W ,j 是矿石编号
这批矿产的检验结果 Y为各个区间的检验值之和。即:
Y = i = 1 m Y i

若这批矿产的检验结果与所给标准值 S 相差太多,就需要再去检验另一批矿产。小 T 不想费时间去检验另一批矿产,所以他想通过调整参数 W 的值,让检验结果尽可能的靠近 标准值 S,即使得 S-Y的绝对值最小。请你帮忙求出这个最小值。
输入格式
输入文件 qc.in。 第一行包含三个整数 n,m,S,分别表示矿石的个数、区间的个数和标准值。 接下来的 n行,每行 2个整数,中间用空格隔开,第 i+1 行表示 i 号矿石的重量 wi 和价 值 vi 。 接下来的 m行,表示区间,每行 2个整数,中间用空格隔开,第 i+n+1 行表示区间[Li, Ri]的两个端点 Li 和 Ri。注意:不同区间可能重合或相互重叠。
输出格式
输出文件名为 qc.out。 输出只有一行,包含一个整数,表示所求的最小值。
样例数据
input
5 3 15
1 5
2 5
3 5
4 5
5 5
1 5
2 4
3 3
output
10
数据规模与约定
【输入输出样例说明】 当 W 选 4 的时候,三个区间上检验值分别为 20、5、0,这批矿产的检验结果为 25,此 时与标准值 S相差最小为 10。 【数据范围】
对于 10%的数据,有 1≤n,m≤10;
对于 30%的数据,有 1≤n,m≤500;
对于 50%的数据,有 1≤n,m≤5,000;
对于 70%的数据,有 1≤n,m≤10,000;
对于 100%的数据,有 1≤n,m≤200,000,0

分析

本质还是二分答案。与其他题不同的是,这道我们需要根据题意设计一个求解Y的函数,而非检查函数,再通过与S比较大小来二分答案。而求解函数需要用到前缀和的累加求解法以减轻时间复杂度,数据类型要开longlong,就可以解决这道题。
代码实现如下:

#include<bits/stdc++.h>
using namespace std;
long long n,m,S,Left,Right,mid,ans=9999999999999999LL,tot=0,Y;
struct stone{long long value,weight;}Stone[200080]={};
struct interval{long long begin,end;}Interval[200080]={};
struct sum{long long num,s;}Sum[200080]={};
inline void input()
{
    cin>>n>>m>>S;
    for(int i=1;i<=n;i++)cin>>Stone[i].weight>>Stone[i].value,tot+=Stone[i].weight;
    for(int i=1;i<=m;i++)cin>>Interval[i].begin>>Interval[i].end;
}
inline long long solve(long long mid)
{
    for(int i=1;i<=n;i++)
    {
        Sum[i]=Sum[i-1];
        if(Stone[i].weight>=mid)
        {
            Sum[i].num++;
            Sum[i].s+=Stone[i].value;
        }
    }
    long long result=0;
    for(int i=1;i<=m;i++)
    {
        result+=(Sum[Interval[i].end].num-Sum[Interval[i].begin-1].num)*(Sum[Interval[i].end].s-Sum[Interval[i].begin-1].s);
    }
    return result;
}
inline void find()
{
    Left=1;
    Right=tot;
    while(Left+1<Right)
    {
        mid=(Left+Right)>>1;
        memset(Sum,0,sizeof(Sum));
        Y=solve(mid);
        if(Y<S)Right=mid;
        else Left=mid;
    }
    Y=solve(Left);
    if(abs(Y-S)<ans)ans=abs(Y-S);
    Y=solve(Right);
    if(abs(Y-S)<ans)ans=abs(Y-S);
    cout<<ans<<endl;
}
int main()
{
    input();
    find();
    return 0;
}

<后记>


<废话>

猜你喜欢

转载自blog.csdn.net/Prasnip_/article/details/80991571