二分——二分查找,二分答案

二分查找

二分查找也称折半查找,它是一种效率极高的查找方法。但是折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列,就是:数据要是有序排列的备注:二分查找的一个非常重要的前提条件就是查找的内容具备单调性

举个例子

假如我们需要在10亿个不同的数字当中找到目标数字那么通过以往学习的知识点我们通过循环查找那么必定会超时,如果我们使用二分查找这种方式的话那么就不会出现超时的情况了。 我们可以先将这些不同的数字排好序,然后找到中间的那个数,如果中间的数字比目标数字大那么我们就可以排除从中间到末尾的所有数字,反之我们可以排除从开头到中间的所有说,每次都按照此方法比较排除那么我们最多只需要30次(10亿介于2的29次方和2的30次方之间)就可以完成该目标的查找了,这样极大的提升了程序的效率。

二分查找算法思想

对于n个有序且没有重复的元素(假设为升序),从中查找特定的某个元素x。

我们可以将有序序列分成规模大致相等的两部分,然后取中间元素与要查找的元素x进行比较,如果x等于中间元素,查找成功算法终止;

如果x小于中间元素,则在序列的前半部分继续查找,否则在序列的后半部分继续查找。

这样就可以将查找的范围缩小一半,然后在剩余的一半中继续重复上面的方法进行查找。

这种每次都从中间元素开始比较,并且一次比较后就能把查找范围缩小一半的方法,叫作二分查找。

时间复杂度O(logN)

二分査找时间复杂度:log2(n)
推导:
因为二分查找每次排除掉…半的不适合值,所以对于n个元素的情况:
一次二分剩下:n/2
两次二分剩下:n/2/2 = n/4
m次二分剩下:n/(2^m)
在最坏情况下是在排除到只剩下最后一个值之后得到结果,即
n/(2^m)=l
所以由上式可得:2^m=n
进而可求出时间复杂度为:log2(n)
注意:
log2(l000000) ≈ 19.9
log2(100000000) ≈ 26.6

二分查找算法描述

用一维数组a存储有序元素序列,用变量l和r分别表示查找范围中第一个元素和最后一个元素的下标,mid表示查找范围的中间位置对应元素的下标,x为要查找的元素。

  1. 变量初始化,令l=1,r=n。l和r分别初始化为有序序列的第一个元素和最后一个元素的下标。

  2. 判断查找范围l≤r是否成立,如果成立,执行(3),否则输出"-1"(表示没有找到),结束算法。

  3. 取中间元素,令mid=(l+r)/2,a[mid]就是中间元素。

  4. 比较a[mid]与x,如果a[mid]等于x,则查找成功,结束算法;如果x<a[mid],则在序列的前半部分进行查找,修改查找的上界r=mid-1,下界不变,否则将在序列的后半部分进行在找,修改查找的下界l=mid+1,上界不变,转到(2)。

<font color = red> 特别注意:使用二分查找时,必须保证数据是有序的,若数据是无序的,则需要使用排序算法将数据变得有序,即上文中提到的单调性

二分查找注意点

  • 二分查找元素X在序列中是否存在
  • 二分查找的左边界:第一次出现的位置(序列中可能存在多个目标值,找到第一个>=X的元素位置)
  • 二分查找的右边界:最后一次出现的位置(序列中可能存在多个目标值,找到最后一个==X的元素位置 或者 第一个大于X的元素位置)

二分查找算法的框架如下:

int fun(int a[],int n,int x) //在一个有n个数的有序数组a中查找目标值x
{
    int l=1,r=n,mid;
    while(l<=r)   //判断查找范围l<=r是否成立 
    {
        mid=(l+r)/2;   //取中间元素的位置 
        if(x==a[mid])    //x已经找到 
        {
            return mid;    //返回x对应的下标 
        }
        else if(x<a[mid])
        {
            r=mid-1;     //调整r,在前半部分查找 
        }
        else l=mid+1;   //调整l,在后半部分查找 
    }
    return -1;
} 

例题1:二分查找

请在一个有序递增数组中(不存在相同元素),采用二分查找,找出值 x的位置,如果 x在数组中不存在,请输出 -1 !
输入数据:
第一行,一个整数 n ,代表数组元素个数(n≤10^6)
第二行,n 个数,代表数组的 n 个递增元素(1≤数组元素值≤10^8 )
第三行,一个整数 x,代表要查找的数(0≤x≤10^8)
输出x 在数组中的位置,或者 -1。
输入数据:

10
1 3 5 7 9 11 13 15 17 19
3

输出数据:

2

解法一:暴力,超时。

思路:使用循环暴力的一个一个的查找

#include<bits/stdc++.h>
using namespace std;
int a[1000005];
int main(){   
    int n,i,f=0,x;
    cin>>n; 
    for(i=1;i<=n;i++)
    {   cin>>a[i];  }
    cin>>x;
    for(i=1;i<=n;i++){   
        if(a[i]==x){   
            cout<<i;
            f=1; break;
        }
    }
    if(f==0){cout<<-1<<endl;}
}

解法二:二分查找

思路:使用二分查找的思路不断的缩小查找范围。

注意:本题的数据量较大,最多达到10^8,对于百万级别及以上的数据量有时候我们使用二分也会出现超时的情况,那是因为使用cin,cout以及换行命令endl的效率比较低,那么我们就要添加ios::sync_with_stdio(0);cin.tie(0);并且把endl更换成"\n"进行优化。具体体现在例题2中。

原因如下:

  • cin,cout之所以效率低,是因为先把要输出的东西存入缓冲区,再输出,导致效率降低,而使用ios::sync_with_stdio(0);与这条语句可以来打消iostream的输入 输出缓存,可以节省许多时间

  • 在标准 C++ 中,cin 和 cout 会同步输出。这意味着,如果你在调用 cin 读取输入之前调用了 cout,那么 cout 的输出会先被缓冲(也就是存储在内存中),直到你调用了 cin 读取输入之后,缓冲中的输出才会被真正输出到屏幕上。

    cin.tie(0); 的作用就是解除这种同步,使得 cout 的输出不再被缓冲,而是直接输出到屏幕上。这样,你就可以在调用 cin 读取输入之前,就可以先调用 cout 输出内容。

  • endl除了换行外还有清空、刷新缓冲区的作用

    对于“刷新缓冲区”的理解:从键盘输入一段字符,不会立即显示在屏幕上,而是先存入缓冲区,再从缓冲区拿出来这样一个过程,而'\n'没有清空、刷新缓冲区的功能即意味着带有'\n'的语句可能比带endl的语句在屏幕上显示出的速度稍慢。带endl的语句会显示的快一点。当然,因为endl比'\n'多一些操作,在效率上endl是慢于'\n'的。

#include<bits/stdc++.h>
using namespace std;
int a[1000005];
int main() {
    ios::sync_with_stdio(0);
    int k, n;
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    cin >> k;
    int left = 1, right = n, mid; //left right分别表示序列中第一个和最后一个元素
    while (left <= right) {
        mid = (left + right) / 2; //找中间值
        if (a[mid] == k) {
            cout << mid;  //找到了 结束程序
            return 0;
        } else if (k < a[mid]) { //目标值小于中间值 继续查找左半部分
            right = mid - 1;
        } else if (k > a[mid]) { //目标值大于中间值 继续查找右半部分
            left = mid + 1;
        }
    }
    cout << -1; //如果都没有找到则输出-1
    return 0;
}

例题2:查找m个数(主题库2646)

请你输入一个含 n个数字的不重复数列,请你高速的在这个数列中寻找 m 个数字 x1,x2,...,x_m ,如果能找到直接输出,如果不存在输出 -1 ,用换行隔开(0<m<n<=10^6)

输入格式:

输入共 4 行,第一行,为一个数字 n。第二行为n个数字。第三行为一个数字m。第四行为m个要寻找的数字。

输出格式:

输出共 \(m\) 行,每行一个数字,如果能找到直接输出原数字,如果找不到,输出 \(-1\) 。

样例输入

5
1 2 3 4 5 
3
2 1 3

样例输出

2
1
3

问题分析

解法一:暴力,样例没问题,提交0分。

#include<bits/stdc++.h> 
using namespace std;
int a[1000001];
int main()
{ 
    int n,m,i,j,t;
    cin>>n; 
    for(i=1;i<=n;i++)
    { 
        cin>>a[i]; 
    }
    cin>>m;
    for(i=1;i<=m;i++)
    { 
        cin>>t;
        for(j=1;j<=n;j++)
        { 
            if(a[j]==t)
            { 
                cout<<j<<endl;
                break;
            }
        }
    }
    return 0;
}

解法2:二分查找,输入超时

#include<bits/stdc++.h>
using namespace std;
const int N = 1000005;
int a[N];
int main() {
    int n, m, i, j, t;
    cin >> n;
    for (i = 1; i <= n; i++) {
        cin >> a[i];
    }
    sort(a + 1, a + 1 + n); //给数组排序,确保单调性前提
    cin >> m;
    for (i = 1; i <= m; i++) {
        cin >> t;
        /*
        left:第一个元素 
        right:最后一个元素 
        mid:中间值 
        f:标记变量 0表示找不到  1表示找到了
        */
        int left = 1, right = n, mid, f = 0; 
        while (left <= right) {
            mid = (left + right) / 2; //找中间值
            if (a[mid] == t) {
                f = 1;  //找到了 修改标记标量 并结束当前查找
                break;
            } else if (t < a[mid]) { //目标值小于中间值 继续查找左半部分
                right = mid - 1;
            } else if (t > a[mid]) { //目标值大于中间值 继续查找右半部分
                left = mid + 1;
            }
        }
        if (f == 1) { //通过判断标记变量输出结果
            cout << t << endl;
        } else {
            cout << -1 << endl;
        }
    }
}

解法3:添加优化语句,代码可以AC

#include<bits/stdc++.h>
using namespace std;
const int N = 1000005;
int a[N];
int main() {
    ios::sync_with_stdio(0); //优化输入输出缓存
    cin.tie(0); //解除cin cout同步
    int n, m, i, j, t;
    cin >> n;
    for (i = 1; i <= n; i++) {
        cin >> a[i];
    }
    sort(a + 1, a + 1 + n);//给数组排序,确保单调性前提
    cin >> m;
    for (i = 1; i <= m; i++) {
        cin >> t;
        /*
        left:第一个元素 
        right:最后一个元素 
        mid:中间值 
        f:标记变量 0表示找不到  1表示找到了
         */
        int left = 1, right = n, mid, f = 0;
        while (left <= right) {
            mid = (left + right) / 2; //找中间值
            if (a[mid] == t) {
                f = 1;
                break;
            } else if (t < a[mid]) { //目标值小于中间值 继续查找左半部分
                right = mid - 1;
            } else if (t > a[mid]) { //目标值大于中间值 继续查找右半部分
                left = mid + 1;
            }
        }
        if (f == 1) { //通过判断标记变量输出结果
            cout << t << "\n"; // "\n"取消输出缓冲器刷新
        } else {
            cout << -1 << "\n";
        }
    }
}

例题3:找朋友(主题库1189)

小学毕业后,同学们都进入了不同的初中,小明非常想念小伙伴们,所以他打算联系小学的同学们。 现在他得到了市内某所初中的所有名单,找出其中小明的小伙伴们。

输入输出格式

输入

第一行一个整数n,表示某初中人数。

接下来n行,每行一个字符串,只有小写字母组成,表示该校每个人的拼音。数据保证没有人拼音相同,且已经按照字典序从小到大排序。

第n+2行有一个整数m,表示小明的小伙伴个数。

最后m行,每行一个字符串,只有小写字母组成,表示每个小伙伴的拼音,同样保证没有重复。

输出

输出所有在该校的小伙伴的拼音。

每行一个拼音,顺序按照小伙伴给出的顺序。

样例输入:

3
alice
bob
zhangsan
2
lisi
zhangsan

样例输出:

zhangsan

数据范围

对于70%的数据,n<=1000,m<=100

对于100%的数据,n<=100000,m<=10000,每个人拼音长度不超过15。

所有数据,学校学生名单中的姓名,都是按照字典序从小到大排序。

问题分析: 本题我们可以使用二分查找+字符串操作实现。学生的名单按照字典序排好是使用二分思想解题的前提条件,然后使用二分方法根据字典序不断缩小查找范围。

#include<bits/stdc++.h>
using namespace std;
int m, n;
string a[100005], t;
//二分查找方法
int fun(string t) {
    int left = 1, right = n, mid;
    while (left <= right) {
        mid = (left + right) / 2;
        if (a[mid] == t) { //如果中间就是目标值 返回中间值下标
            return mid;
        } else if (t > a[mid]) {
            left = mid + 1;
        } else if (t < a[mid]) {
            right = mid - 1;
        }
    }
    return -1;
}
int main() {
    ios::sync_with_stdio(0); //优化输入输出缓存
    cin.tie(0); //解除cin cout同步
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    cin >> m;
    for (int i = 1; i <= m; i++) {
        cin >> t;
        if (fun(t) != -1) { //如果能找到输出这个人姓名
            cout << t << endl;
        }
    }
    return 0;
}

习题训练:和为给定数(主题库2631)

二分查找左边界

题目描述

请在一个有序不递减的数组中(数组中可能有相等的值),采用二分查找,找到值 x第 1 次出现的位置,如果不存在 x请输出 -1。
请注意:本题要求出 q个 x,每个 x在数组中第一次出现的位置。
比如有 6 个数,分别是:1,2,2,2,3,3那么如果要求找 3 个数:3,2,5在数组中第一次出现的位置,答案是:5,2,-1
输入格式
第一行,一个整数 n,代表数组元素个数(n≤10^5)
第二行,n个整数,用空格隔开,代表数组的 n 个元素(1≤数组元素的≤10^8 )
第三行,一个整数 q,代表有要求出 q个数首次出现的位置(q≤10^5)
第四行,q 个整数,用空格隔开,代表要找的数(1≤要找的数≤10^8 )
输出格式
输出一行,含 q 个整数,按题意输出要找的每个数在数组中首次出现的位置,如果不存在这样的数,请输出 -1。

输入数据#1
6
1 2 2 2 3 3
3
3 2 5
输出数据#1
5 2 -1

二分査找左边界注意点:

(1) 二分查找,如果a[mid] == x,还要向左侧看;
(2) 找左边界的本质:找数组中第一个>=X的元素的位置;
(3) 找到位置L之后,要判断a[L]==x (注意,如果都是负数,找0,要判断L在下标范围内)

问题分析:

#include<bits/stdc++.h>
using namespace std;
long long a[100005], n, q, t;
int fun(int x) {
    int left = 1, right = n, mid;
    while (left <= right) {
        mid = (left + right) / 2;
        if (a[mid] == x) {
            //查找左边界的核心,找到正确答案之后继续往左边查找,看看是否还存在等于X的值
            right = mid - 1;
        } else if (x < a[mid]) {
            right = mid - 1;
        } else if (x > a[mid]) {
            left = mid + 1;
        }
    }
    /*
    如果该值存在于数组中,那么在找到相等值后right边界会不断向左收缩,直到right索引位于X目标值的左边一个索引,而退出循环的条件刚     好是left=right+1,刚好是X的左侧边界索引值!
    */
    if (a[left] == x) {
        return left;
    } else {
        return -1;
    }
}
int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    cin >> q;
    for (int i = 1; i <= q; i++) {
        cin >> t;
        cout << fun(t) << " ";
    }
}

关于左边界解题的FAQ:

Q:为什么left是正确的返回值?

A:如果该值存在于数组中,那么在找到相等值后right边界会不断向左收缩,直到right索引位于X目标值的左边一个索引,而退出循环的条件刚好是left=right+1,刚好是X的左侧边界索引值!

Q:为什么没有找到时能正确返回-1?

A:没有找到X返回-1时有两种情况

  • ​ 一种是X大于数组中的所有值,如果X大于数组中的所有值,那么right永远不会改变,即right = n,left边界会不断收缩,直到退出循环时left=right+1,即left=n+1;此时left超出边界,不能用a[left] != X进行判断。
  • 另一种情况是X没有超出边界,但是不存在,这时只需要判断left索引对应的值是否等于X即可。这里还能判断X小于数组中所有值的情况,因为这时left的值始终不会改变,没有超出边界,可以通过a[left] != X进行判断。

二分查找右边界

题目描述

请在一个有序不递减的数组中(数组中可能有相等的值),采用二分查找,找到值 x最后 1次出现的位置,如果不存在 x请输出 -1。
请注意:本题要求出 q个 x,每个 x 在数组中最后一次出现的位置。
比如有 6 个数,分别是:1,2,2,2,3,3那么如果要求找 3 个数:3,2,5,在数组中最后一次出现的位置,答案是:6,4,-1
输入格式:
第一行,一个整数 n,代表数组元素个数(n≤10^5)
第二行,n 个整数,用空格隔开,代表数组的 n 个元素(1≤数组元素的值≤10^8 )
第三行,一个整数 q,代表有要求出 q 个数最后一次出现的位置(q≤10^5)
第四行,q个整数,用空格隔开,代表要找的数(1≤要找的数 ≤10^8)
输出格式:
输出一行,含 q个整数,按题意输出要找的每个数在数组中最后一次出现的位置,如果不存在这样的数,请输出 -1。
输入数据#1

6
1 2 2 2 3 3
3
3 2 5

输出数据#1

6 4 -1

二分查找右边界注意点:
(1) 二分查找,如果a[mid] == x,还要向右侧看,判断右侧是否还是x;
(2) 找右边界的本质:找的值L,是数组中第一个>x的元素的位置;
(3) 因此要判断L-1 (或者R)的值是否和x相等(a[L-1]==x);

程序如下∶

#include<bits/stdc++.h>
using namespace std;
int a[1000010],p;
int n,q;
int f(int x){
    int left=1,right=n,mid;
    while(left<=right){
        mid=(left+right)/2;
        if(a[mid]>x){
            right=mid-1;
        } 
        else if(x>a[mid]){
            left=mid+1;
        } 
        //查找右边界的核心,找到正确答案之后继续往右边查找,看看是否还存在等于X的值
        else if(x==a[mid]){
            left=mid+1;
        } 
    }
    /*
    如果该值存在于数组中,那么在找到相等值后left边界会不断向右收缩,直到left索引位于目标值X的右边一个索引,而退出循环的条     件刚好是left=right+1,即right=left-1,right刚好是X的右侧边界索引值!
    */
    if(a[right]==x) {
        return right;
    }
    else {
        return -1;
    }
}
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>a[i];
    }
    cin>>q;
    for(int i=1;i<=q;i++){
        cin>>p;
        cout<<f(p)<<" ";
    }
    return 0;
}

关于右边界解题的FAQ:

Q:为什么right是正确的返回值?

A:如果该值存在于数组中,那么在找到相等值后left边界会不断向右收缩,直到left索引位于目标值的X右边一个索引,而退出循环的条件刚好是left=right+1,即right=left-1,right刚好是X的右侧边界索引值!

Q:为什么没有找到时能正确返回-1?

A:没有找到X返回-1时有两种情况:

  • 一种是target小于数组中的所有值,如果X小于数组中的所有值,那么left永远不会改变,即left = 0,right边界会不断收缩,直到退出循环时right=left-1,即right=-1;此时right超出边界,不能用a[right] != X进行判断。
  • 另一种情况是X没有超出边界,但是不存在,这时只需要判断right索引对应的值是否等于X即可。这里还能判断X大于数组中所有值的情况,因为这时right的值始终不会改变,没有超出边界,可以通过a[right] != X进行判断。

二分函数(参考)

binary_search():二分查找函数

binary_search(a+开始, a+结束+1, x, cmp);
函数含义:在a数组的下标区间内,按照cmp的排序规则,找元素x,找到返回true,找不到返回false。
注意点:
(1)查找区间:结束位置后一个位置,和sort()函数一致;
(2)排序规则cmp可以不写默认是升序。如果写的话,查找时的排序规则,必须和排序的规则是一致的;

例子:
#include<bits/stdc++.h>
using namespace std;
int cmp(int x,int y){
    return x>y;
}
int main(){
    int a[6]={20,10,50,30,60,40};
    sort(a+0,a+5+1);
    cout << binary_search(a+0,a+5+1,20)<< endl;
    cout << binary_search(a+0,a+5+1,36)<<endl; 
    sort(a,a+6,cmp);
    cout << binary_search(a+0,a+5+1,20)<<endl; 
    cout << binary_search(a+0,a+5+1,20,cmp)<<endl; 
    return 0;
}

输出结果:
1
0
0
1

lower_bound():二分査找左边界

lower_bound(a+开始,a+结束+1,x,cmp);

函数含义:在a数组的下标区间内,按照cmp的排序规则,找元素x的左边界(第一个 >=元素x的位置),返回位置指针;(指针(Pointer): 变量的地址,通过它能找到以它为地址的内存单元。)

注意点:
(1)基本注意点同binary_search;
(2)此处返回的不是下标的值,而是返回指针;如果找不到符合条件的元素位置,指向下标为第一个大于元素的位置

例子:

#include<bits/stdc++.h>
using namespace std;
int main(){
    int a[6]={20,10,50,20,20,40};
    sort(a,a+5+1);//10 20 20 20 40 50  
    int *p=lower_bound(a+0,a+5+1,20);
//  cout << p << " "<< *p << endl;
//  cout << p-a<< endl; 
    cout << lower_bound(a,a+6,20)-a << endl;
    cout << lower_bound(a,a+6,15)-a << endl;
    cout << lower_bound(a,a+6,60)-a << endl;
    return 0;
}

upper_bound:二分查找右边界

upper_bound(a+开始, a+结束+1,x,cmp);

函数含义:在a数组的下标内,按照cmp的排序规则,找元素x的 右边界(第一个 > 元素x的位置),返回位置指针;
注意点:同 lower_bound;

例子:

#include<bits/stdc++.h>
using namespace std;
int main(){
    int a[6]={20,10,50,20,20,40};
    sort(a,a+5+1);
    int *p=upper_bound(a,a+6,20); 
    cout << p << " "<< *p << endl;
    cout << p-a<< endl; 
    cout << upper_bound(a,a+6,20)-a << endl;
    cout << upper_bound(a,a+6,15)-a << endl;
    cout << upper_bound(a,a+6,60)-a << endl;
    return 0;
}

本章作业

二分查找左边界
序列询问
二分查找右边界
二分查找满足条件的数
查找m个数字
同时出现的数
数的范围
和为给定数
找朋友

二分答案

我们可以根据题目的已知条件设定答案的上下界,然后用二分的方法枚举答案,再判断答案是否可行,根据判断的结果逐步缩小答案范围,直到找到符合题目条件的答案为止。二分答案常被用来求解最小值最大最大值最小等最值问题,将最优化问题转换为判定问题。

适用条件:
单调性∶问题的答案具有单调性。
枚举可求解∶问题的答案可以通过枚举求解,可以将二分答案理解为枚举的一种优化。

基本步骤:
确定答案范围∶确定问题答案可能的最小值和最大值。
确定上下界∶确定问题是求上界还是求下界。
判定方案∶确定判定方案,编写判定函数(check函数)。所谓的check()函数就是用于检查当前的答案是否符合题目要求,若符合则继续寻找更优的解,若不符合则在另一个半区寻找答案

例题1:砍伐树木

题目描述

伐木工人小科需要砍倒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<=10^6),M表示需要的木材总长度(1<=M<=2 * 10^9)
第2行:N个整数表示每棵树的高度,值均不超过10^9。所有木材长度之和大于M,因此必有解。;
输出
1个整数,表示砍树的最高高度。

输入样例
5 20
4 42 40 26 46
输出
36

思路:本题要求得到长度至少M米的木材,那么只要我们 我们能够得到M米的木材的基础上把锯片升高,即当前的答案符合条件的时候适当增加数值(l=mid+1)然后验证新的数值是否也符合题目要求,即查找答案的右边界 。即

if(t >= m)//锯的木材不少于m
{
    l=mid+1; 
}
参考代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1000005;
long long a[N],n,m;

//check函数思路:当锯片高度为x的时候计算得到的木材量。 
long long check(long long x)
{
    long long s = 0;
    for (int i = 1; i <= n; i++)
    {
        if (x < a[i]) //如果当前的树木可以锯
        {
            s = s + a[i] - x;//计算锯下来的长度 并求和
        }
    }
    return s;
}
int main() 
{
    cin >> n >> m;
    long long l=0,r,mid;
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
        r=max(a[i],r); //找到答案范围的右边界
    }
    while (l <= r)
    {
        mid = (l + r) / 2;
        long long t=check(mid);//
        if (t == m)//锯片高度为mid获取的木材刚好为m 
        { 
            cout<<mid;//mid是最优值 结束查找
                        return 0; 
        }
        else if(t<m)
        {
            r = mid - 1;//设定高度低一点,争取满足m米木材 
        }
        else if(t>m)//获取的木材较多 ,那就提高锯片高度 
        {
            l=mid+1;
                }
    }
    cout << r;//右端点 
    return 0;
}

例题2:跳石头

一年一度的“跳石头”比赛又要开始了!
这项比赛将在一条笔直的河道中进行,河道中分布着一些巨大岩石。组委会已经选择好了两块岩石作为比赛起点和终点。在起点和终点之间,有 N 块岩石(不含起点和终点的岩石)。在比赛过程中,选手们将从起点出发,每一步跳向相邻的岩石,直至到达终点。
为了提高比赛难度,组委会计划移走一些岩石,使得选手们在比赛过程中的最短跳跃距离尽可能长。由于预算限制,组委会至多从起点和终点之间移走 M 块岩石(不能移走起点和终点的岩石)。

输入

第一行包含三个整数 L,N,M ,分别表示起点到终点的距离,起点和终点之间的岩石数,以及组委会至多移走的岩石数。保证L≥1 且 N≥M≥0 。
接下来 N 行,每行一个整数,第 i 行的整数 Di( 0 < Di < L), 表示第 i 块岩石与起点的距离。这些岩石按与起点距离从小到大的顺序给出,且不会有两个岩石出现在同一个位置。

输出

一个整数,即最短跳跃距离的最大值。
样例

输入样例
25 5 2
2
11
14
17
21
输出样例
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 。

解题思路:我们可以每次设置一个距离为x,然后使用check()函数去检查当前的距离为x时河道中需要放置几块石头,当放置的石头的数量t<=m时则当前的距离x可以作为一个答案,那么我们接下来可以继续增加距离值x查找是否有更优解。反之缩小距离x查找。

参考答案
/****by zss****/
// 二分查找右边界 
#include<bits/stdc++.h>
using namespace std;
int a[50005];
int L,M,N; 
int find(int x)//x为枚举的距离
{
    int s=0,len=0;
    for(int i=1;i<=N;i++)
    {
        if(a[i]-len < x)//当前点距离上一个跳越点的距离 小于x
        {//可以跳,那就拿走这块石头
            s++;
        }
        else
        {
            len=a[i];//不能拿,更新当前跳跃点 
        }
    }
    return s;
}
int main()
{
    cin>>L>>N>>M;
    for(int i=1;i<=N;i++)cin>>a[i];
    int l=0,r=L;
    while(l<=r)
    {
        int mid=(l+r)/2;//枚举mid为跳跃的最短距离
        //开始判断以mid为最短距离所减少的石头数量t
        int t=find(mid);
        if(t>M)//移走的石头太多了,最短距离要缩小 
        {
            r=mid-1;    
        } 
        else if(t==M)//移走的石头与m相等,那就尽可能的扩大距离 
        {
            l=mid+1; 
        }
        else//移走的石头少了,只能扩大距离,增加移走的石头数量
            l=mid+1; 
    }
    cout<<r; 
    return 0;
}




作业

木板打地鼠

看小说

郊游活动

运输木材

posted @ 2024-04-08 13:45  Befrepof  阅读(117)  评论(0编辑  收藏  举报