二分查找与二分答案

目录

二分查找

模板

例题:1. 两数之和 - 力扣(LeetCode)

lower_bound( )和upper_bound( )

例题:806 div4  Problem - F - Codeforces

特别应用(范型写法)

例题:34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)

二分答案

例题一:[美国国际航空公司 2016年1月]愤怒的奶牛 (nowcoder.com)

例题二:跳石头 (nowcoder.com)

例题三:[NOIP2012]借教室 (nowcoder.com)


二分查找

二分查找是一种在有序数组中查找某一特定元素的搜索算法。

使用前提:1.数列有序。   2.数列使用顺序存储结构。

时间复杂度:O(logn) 

证明:假设有n个数,经过k次二分查找,每次减少一半,直到最后剩下一个数,即n,n/2,n/4...1,也就是n*[(1/2)^k]=1,解得k=log2 n

模板

关键点:

  1. while(l<=r)   小于等于。
  2. mid=(l+r)>>1也可以写成mid=l+((r-l)>>1),两种写法看题目类型选取,第二种写法一定不会出错,既可用于实数情况,也可用于数组情况,但第一种写法最好只用于数组情况,因为如果数据量太大,l+r可能就爆int范围了。
  3. l=mid+1,r=mid-1,写错容易死循环。
int Binary_search(int a[n],int t) {
    //左指针指向数组的第一个数,右指针指向数组的最后一个数
    int l = 1, r = n, mid;
    while(l<=r){
        mid=(l+r)>>1;
        if(a[mid]==t){
            return mid;
        }
        else if(a[mid]>t){
            r=mid-1;
        }
        else{
            l=mid+1;
        }
    }
    return l;
}

对于二分查找,如果可以找到这个数,那么就返回这个数的下标;如果找不到这个数,那么一定是由于不满足while循环的条件而退出循环,所以左指针在右指针的右边(r,l),左指针指向第一个一个大于它的数(也是这个数应该出现的位置),右指针指向最后一个小于它的数,返回值返回左指针还是右指针看具体题目来定。要特别注意:如果要查找的数不存在,并且它比非递减数列的第一个数还要小,那么此时左指针指向第一个数,若其下标为0,那么右指针的值则为-1,如果返回的是右指针,一定在主函数中要特判一下,不能直接写a[r],这样会导致越界问题出现;同理,如果要查找的数比非递减数列的最后一个数还要大,那么右指针指向最后一个数,左指针越界,返回左指针时要特判。

例题:1. 两数之和 - 力扣(LeetCode)

对于一个数a,要去数组中查找target-a,很显然我们会想到二分查找,但二分查找的前提是有序数组,所以要先给数组排序,由于要返回数组下标,所以排序时原下标不能丢,可以开一个结构体存数值和下标,也可以直接用再开一个数组存下标,对这个数组根据原数组的大小进行排序。也可以直接利用unordered_map(元素可重复)的find函数去查找。

代码如下:

class Solution {
public:
    int binarysearch(vector<int>& nums,vector<int>& Id,int i,int target)
    {
        int l=i,r=Id.size()-1,mid;
        while(l<=r)
        {
            mid=(l+r)/2;
            if(nums[Id[mid]]>target)
            {
                r=mid-1;
            }
            else if(nums[Id[mid]]<target) 
            {
                l=mid+1;
            }
            else{
                return mid;
            }
        }
        return -1;
    }
    vector<int> twoSum(vector<int>& nums, int target)
    {
        vector<int>ans(2);
        vector<int>Id(nums.size());
        int i,ret;
        for(i=0;i<nums.size();i++)
        {
            Id[i]=i;
        }
        //不改变原数组的顺序,Id中存储原数组从小到大排序后各元素的下标
        sort(Id.begin(),Id.end(),[&](int i,int j){return nums[i]<nums[j];});
       for(i=0;i<Id.size();i++)
       {
            //传i+1是因为循环没结束,所以前面已经遍历过的数不可能和现在的数匹配上,
           //所以左指针不需要从第一个数开始找,直接从它后一个开始就可以
            ret=binarysearch(nums,Id,i+1,target-nums[Id[i]]);
            if(ret!=-1)
            {
                ans[0]=Id[i];
                ans[1]=Id[ret];
                break;
            }
       }
       return ans;
    }
};

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        vector<int>ans(2);
        unordered_map<int,int>t;
        int i;
        for(i=0;i<nums.size();i++){
            t.insert(make_pair(nums[i],i));
        }
        for(i=0;i<nums.size();i++){
            //找到了,并且不是同一个
            if(t.find(target-nums[i])!=t.end()&&t.find(target-nums[i])->second!=i){
                ans[0]=i;
                ans[1]=t.find(target-nums[i])->second;
            }
        }
        return ans;
    }
};

lower_bound( )和upper_bound( )

二分查找也可以直接使用lower_bound( )和upper_bound( )。

lower_bound( )

常规版本:在一个非递减的数组中从first位置到last-1位置去查找第一个大于等于val的数,找到了就返回这个数的地址,找不到则返回last。

template <class ForwardIterator, class T>
  ForwardIterator lower_bound (ForwardIterator first, ForwardIterator last,
                               const T& val);

重载版本:在一个非递增的数组中从first位置到last-1位置去查找第一个小于等于val的数,找到了就返回这个数的地址,找不到则返回last。区别仅仅只有数组整体是递增的还是递减的和>=与<=,但我们需要在函数参数中多加一个仿函数greater<T>() 。

template <class ForwardIterator, class T, class Compare>
  ForwardIterator lower_bound (ForwardIterator first, ForwardIterator last,
                               const T& val, Compare comp);

如果想要直接得到元素的下标,而不是它的地址(一个指向它的指针),我们直接用返回地址减去起始地址即可。

例:

#include<iostream>
#include<algorithm>
#include<functional>
using namespace std;
int main() {
    int a1[8] = { 10,20,30,40,50,60,70,80 };
    int a2[8] = { 80,70,60,50,40,30,20,10 };
    int pos1 = lower_bound(a1, a1 + 8, 20) - a1;
    int pos2 = lower_bound(a2, a2 + 8, 50, greater<int>()) - a2;
    cout << pos1 << endl;
    cout << pos2 << endl;
    return 0;
}

upper_bound( )

(其实与lower_bound基本一致,区别仅仅只是它没有等号)

常规版本:在一个非递减的数组中从first位置到last-1位置去查找第一个大于val的数,找到了就返回这个数的地址,找不到则返回last。

template <class ForwardIterator, class T>
  ForwardIterator upper_bound (ForwardIterator first, ForwardIterator last,
                               const T& val);

重载版本:在一个非递增的数组中从first位置到last-1位置去查找第一个小于val的数,找到了就返回这个数的地址,找不到则返回last。区别仅仅只有数组整体是递增的还是递减的和>=与<=,但我们需要在函数参数中多加一个仿函数greater<T>() 。

template <class ForwardIterator, class T, class Compare>
  ForwardIterator upper_bound (ForwardIterator first, ForwardIterator last,
                               const T& val, Compare comp);

例题:806 div4  Problem - F - Codeforces

你会得到一个数组 a1,a2,...。计算指数 1≤i,j≤n 对数,使 ai<i<aj<j。

输入
第一行包含一个整数 t (1≤t≤1000) — 测试用例的数量。

每个测试用例的第一行包含一个整数 n (2≤n≤2⋅105) — 数组的长度。

每个测试用例的第二行包含 n 个整数 a1,a2,...,an (0≤ai≤109) — 数组的元素。

保证所有测试用例中的 n 之和不超过 2⋅105。

输出

对于每个测试用例,输出一个整数 — 满足语句中条件的索引对数。

请注意,某些测试用例的答案不适合 32 位整数类型,因此您应该在编程语言中使用至少 64 位整数类型(如 long long 表示 C++)。

题解:首先由于要满足 ai<i<aj<j,对于ai<i和aj<j我们很简单就可以判断出来,如果输入的那个数大于等于它的下标(i,j是从1开始的),我们直接就跳过这个数,那么我们最后就只需要找满足i<aj的数了。由于i、j的含义是数组中的第几个数,因为有i<j这个要求在,所以j一定在i的后面,我们遍历数组中的数时,只需要找在它前面的,并且和aj这个数比较的是i而不是ai,所以我们可以直接再开一个数组存前面的数的下标(一定是递增的),二分查找第一个大于等于它的i,如果i前面还有下标,那前面的一定是满足i<aj的。

代码如下:

#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
#define ll long long
int a[200005];
int main(){
    ios::sync_with_stdio(false);
    cout.tie(0);cin.tie(0);
    int t,n,i;
    cin>>t;
    while(t--){
        cin>>n;
        vector<int>v;
        ll ans=0;
        for(i=1;i<=n;i++){
            cin>>a[i];
        }
        for(i=1;i<=n;i++){
            if(a[i]>=i)continue;
            //得到的是第一个大于等于aj的i在v里面的下标,
            //若为0说明前面没数,为1说明前面有一个数小于aj...
            ans+=lower_bound(v.begin(),v.end(),a[i])-v.begin();
            v.push_back(i);
        }
        cout<<ans<<endl;
    }
    return 0;
}

 

特别应用(范型写法)

例:在0000111111中查找最后一个0,第一个1。

模板:

区别在于:当查找到目标值时,并不立即返回其下标,而是在其上进一步进行查找,返回值根据r在l的左边来确定(也需要注意特判)。

例如在查找最后一个0时,当我们查找到0时,并不能肯定它就是最后一个0,所以我们还要继续往右找,也就是说要把return mid改成l=mid+1,返回值就是return r。

在查找第一个1时,当我们查找到1时,并不能肯定它就是第一个1,所以我们还要继续往左找,也就是说要把return mid改成r=mid-1,返回值就是return l。

并且,查找第一个1,最后一个0这样的题目,代码也不需要写两个函数,完成其中一个即可。例如我们写了查找第一个1这个函数,那么我们显然就能得到最后一个0的下标一定是第一个1的下标减去1,举一个更普适的例子,223336666,我们查找最后一个3,那么主函数中直接调用查找第一个4的函数就可以了,因为如果数组中有4,那么和上面000111111的例子相同,而如果数组中没有4,前面也说了,左指针指向第一个大于它的数,右指针指向最后一个小于它的数,而函数返回的是左指针,也就是第一个6,减一也就是右指针了,指向最后一个小于它的数3,也就是我们所找的最后一个3。

代码如下:

//第一个1                       len是数组大小
int Binary_Search(int a[], int target,int len) {
    int l = 0, r = len - 1, mid;
    while (l <= r) {
        mid = l + ((r - l) >> 1);
        /*
        if (a[mid] == target) {
            r = mid - 1;
        }
        else if (a[mid] > target) {
            r = mid - 1;
        }
        else {
            l = mid + 1;
        }
        */
        //合并后:
        if (a[mid] < target) {
            l = mid + 1;
        }
        else {
            r = mid - 1;
        }
    }
    return l;
}

例题:34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)

 前面说的两个函数写哪个都行,如果用查找最后一个0,结束位置就正常查找,开始位置就查找target-1的结束位置+1即可。

代码如下:(查找第一个1)

class Solution {
public:
    int binarysearch(vector<int>& nums, int target){
        int l=0,r=nums.size()-1,mid;
        while(l<=r){
            mid=(l+r)/2;
            if(nums[mid]<target){
                l=mid+1;
            }
            //找到了也要继续往左找
            else{
                r=mid-1;
            }
        }
        return l;
    }
    vector<int> searchRange(vector<int>& nums, int target) {
        vector<int>v={-1,-1};
  //数组为空    
  //特判的左指针越界,说明没有找到,要找的数比最大的还大 
  //虽然左指针没有越界,但它对应的值和要找的数不一样,说明这个数不存在,左指针指向第一个大于它的数  
if(nums.empty()||binarysearch(nums,target)>=nums.size()||nums[binarysearch(nums,target)]!=target){
            return v;
        }
        v[0]=binarysearch(nums,target);
        v[1]=binarysearch(nums,target+1)-1;
        return v;
    }
};

(查找最后一个0):

class Solution {
public:
    //最后一个0
    int Binary_Search(vector<int>& nums,int target){
        int l=0,r=nums.size()-1,mid;
        while(l<=r){
            mid=l+((r-l)>>1);
            if(nums[mid]>target){
                r=mid-1;
            }
            else{
                l=mid+1;
            }
        }
        return r;
    }
    vector<int> searchRange(vector<int>& nums, int target) {
        vector<int>ans{-1,-1};
        int p1=Binary_Search(nums,target-1)+1,p2=Binary_Search(nums,target);
        //数组为空
        //特判的右指针越界,要找的值比数组中最小的数还要小
        //要找的数不存在
        if(nums.empty()||p2<0||nums[p2]!=target){
            return ans;
        }
        ans[0]=p1;
        ans[1]=p2;
        return ans;
    }
};

——————————————————————————————————————————

二分答案

简而言之就是答案在一个区间里,我们通过二分选择一个数带入题目,如果不符合就直接砍掉一半的区间,如果符合就继续往一侧区间去找最优解。

使用条件:

1、答案在一个区间内。

2、直接搜索不好搜,但是容易判断一个答案可不可行。

3、该区间对题目具有单调性,即:在区间中的值越大或越小,题目中的某个量对应增加或减少。其实也就是可以根据选择的数可不可行来选择下一个区间。

典型特征:求...最大值的最小 、 求...最小值的最大。 

根据题目理解:

例题一:[美国国际航空公司 2016年1月]愤怒的奶牛 (nowcoder.com)

题解:首先题目给了我们所有干草的位置,很明显需要先对它们进行排序。接下来我们就需要思考半径为多少才能把这些坐标全部覆盖,并且还要让半径尽可能的小,我们很简单就能想到半径的取值范围,最小是1,最大是最远的那个点的坐标,但直接做我们也不太知道该从哪开始,这已经很符合二分答案的特征了,我们进一步去思考,如果我们选择了一个半径,用它引爆所有的干草所需要的奶牛个数如果超过了k,那么很显然这个半径选小了,比这个半径更小的那一半就更不可能了,可以直接舍掉一半的区间;而如果它所需要的奶牛个数小于等于k,那么这就是符合题意的解了,它说明选择的半径可能偏大了,我们并不能保证它就是最优解,所以我们继续往它的左边(让半径减小)寻找看有没有更优解。

代码如下:

#include<iostream>
#include<algorithm>
using namespace std;
int a[50005];
int main(){
    int n,k,i,l=1,r,mid,ans;
    cin>>n>>k;
    for(i=0;i<n;i++){
        cin>>a[i];
    }
    sort(a,a+n);
    r=a[n-1];
    while(l<=r){
        int sum=0,begin=0,num=1;
        mid=(l+r)>>1;
//选第一个点作为起点,如果目前覆盖范围小于2*mid,就继续添入下一个点,
//直至若再加入下一个点会导致距离超过最远可覆盖范围,就把这个点当成新起点此时奶牛个数加1
        for(i=0;i<n;i++){
            sum=a[i]-a[begin];
            if(sum>2*mid){
                begin=i;
                num++;
            }
        }
        if(num<=k){
            r=mid-1;
            ans=mid;
        }
        else{
            l=mid+1;
        }
    }
    cout<<ans;
    return 0;
}

例题二:跳石头 (nowcoder.com)

题目描述

一年一度的“跳石头”比赛又要开始了!

这项比赛将在一条笔直的河道中进行,河道中分布着一些巨大岩石。组委会已经选择好了两块岩石作为比赛起点和终点。在起点和终点之间,有 N 块岩石(不含起点和终点的岩石)。在比赛过程中,选手们将从起点出发,每一步跳向相邻的岩石,直至到达终点。

为了提高比赛难度,组委会计划移走一些岩石,使得选手们在比赛过程中的最短跳跃距离尽可能长。由于预算限制,组委会至多从起点和终点之间移走 M 块岩石(不能移走起点和终点的岩石)。

输入描述:

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

输出描述:

输出文件只包含一个整数,即最短跳跃距离的最大值。

输入

25 5 2
2
11
14
17
21

输出

4

说明

将与起点距离为 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。

题解:我们要让最短跳跃距离尽可能长,也就是最长的最短跳跃距离,正好是二分答案的标志词,我们接下来分析是否可以使用二分答案完成此题。首先,最短跳跃距离最小取所有石块间距最小的值,最大取起点石头到终点石头的距离。我们用变量now记录当前所处位置,从now往前跳,如果它和前面那块石头的距离小于我们所选取的最短距离,我们就把这块石头移开,因为最短距离已经定了,不能比它更小了,移开的石头数量+1,以此类推,要注意的是最后从now到终点石头要特别拿出来判断,因为终点石头不能移走,所以不能和上面的过程写在一个循环中,不然终点石头就可能会被移走。最后,如果移走的石头个数大于M,说明移走的过多,最短距离太长了,那么比这个距离还长的区间就可以直接舍去了,因为它们只会导致更多的石头被移走;如果移走的石头个数小于等于M,说明移走的偏少,最短距离取短了,我们就到它右侧取寻找更优解。

代码如下:

#include<iostream>
#include<limits.h>
using namespace std;
int a[50005];
int main() {
    int L, N, M, i, ans, l = INT_MAX, r = 0, mid;
    cin >> L >> N >> M;
    for (i = 1; i <= N; i++) {
        cin >> a[i];
        l = min(l, a[i] - a[i - 1]);
    }
    a[N+1]=L;
    r = L;
    l=min(l,a[N+1]-a[N]);
    while (l <= r) {
        mid = (l + r) >> 1;
        //now记录当前所处位置,num记录移走的石块个数
        int now = 0, num = 0;
        for (i = 1; i <= N; i++) {
            if (a[i] - a[now] < mid) {
                num++;
            }
            else {
                now = i;
            }
        }
      //终点特判:终点不应该被移走,所以如果终点在所选mid中需要被移走说明这种情况不成立,舍去
        if((a[N+1]-a[now]<mid)||num>M){
            r=mid-1;
        }
        else {
            l = mid + 1;
            ans = mid;
        }
    }
    cout << ans;
    return 0;
}

例题三:[NOIP2012]借教室 (nowcoder.com)

题目描述

    在大学期间,经常需要租借教室。大到院系举办活动,小到学习小组自习讨论,都需要向学校申请借教室。教室的大小功能不同,借教室人的身份不同,借教室的手续也不一样。

    面对海量租借教室的信息,我们自然希望编程解决这个问题。

    我们需要处理接下来n天的借教室信息,其中第i天学校有ri个教室可供租借。共有m份订单,每份订单用三个正整数描述,分别为dj, sj, tj,表示某租借者需要从第sj天到第tj天租借教室(包括第sj天和第tj天),每天需要租借dj个教室。

    我们假定,租借者对教室的大小、地点没有要求。即对于每份订单,我们只需要每天提供dj个教室,而它们具体是哪些教室,每天是否是相同的教室则不用考虑。

    借教室的原则是先到先得,也就是说我们要按照订单的先后顺序依次为每份订单分配教室。如果在分配的过程中遇到一份订单无法完全满足,则需要停止教室的分配,通知当前申请人修改订单。这里的无法满足指从第sj天到第tj天中有至少一天剩余的教室数量不足dj个。

    现在我们需要知道,是否会有订单无法完全满足。如果有,需要通知哪一个申请人修改订单。

输入描述:

第一行包含两个正整数n, m,表示天数和订单的数量。

第二行包含n个正整数,其中第i个数为ri,表示第i天可用于租借的教室数量。

接下来有m行,每行包含三个正整数dj, sj, tj,表示租借的数量,租借开始、结束分别在第几天。

每行相邻的两个数之间均用一个空格隔开。天数与订单均用从1开始的整数编号。

输出描述:

如果所有订单均可满足,则输出只有一行,包含一个整数0。否则(订单无法完全满足)输出两行,第一行输出一个负整数-1,第二行输出需要修改订单的申请人编号。

输入

4 3
2 5 4 3
2 1 3
3 2 4
4 2 4

输出

-1
2

说明

第1 份订单满足后,4 天剩余的教室数分别为0,3,2,3。
第2 份订单要求第2 天到第4 天每天提供3 个教室,而第3 天剩余的教室数为2,因此无法满足。分配停止,通知第2个申请人修改订单。

备注:

对于10%的数据,有1≤n,m≤10;

对于30%的数据,有1≤n,m≤1000;

对于70%的数据,有1≤n,m≤105;

对于100%的数据,有1≤n, m≤106, 0≤ri, dj≤109, 1≤sj≤tj≤ n。

题解:首先,完成每个订单后sj到tj的教室数量都会减少dj个,对于这种同时修改一段区间的问题,我们很明显要用到差分。然后我们在考虑二分答案,区间最小值是1,最大值是最后一个订单m,然后我们选一个订单作为无法满足的订单,如果最后check发现它可以被满足,那么他左边的一定都可以满足,不然到不了它,如果他不能被满足,那么我们就往它左边找,看谁是第一个不能被满足的订单。

代码如下:

#include<iostream>
using namespace std;
const int Max = 1e6 + 5;
int a[Max];//原数组
int d[Max];//差分数组
int D[Max];
struct ding {
	int d, s, t;
}b[Max];
int n, m, i;
bool check(int mid) {
	int i;
//用d[]赋值D[],对D进行修改
//因为我们如果我们直接对d进行修改,那么下一次使用check函数,差分数组就不再是一开始的数组了,而是被修改后的数组
	for (i = 1; i <= n; i++) {
		D[i] = d[i];
	}
//修改差分数组
	for (i = 1; i <= mid; i++) {
		D[b[i].s] -= b[i].d;
		D[b[i].t + 1] += b[i].d;
	}
//差分是前缀和的逆运算
	for (i = 1; i <= n; i++) {
		a[i] = D[i] + a[i - 1];
        //说明订单不能被满足
		if (a[i] < 0) {
			return true;
		}
	}
	return false;
}
int main() {
	cin >> n >> m;
	for (i = 1; i <= n; i++) {
		cin >> a[i];
		d[i] = a[i] - a[i - 1];
	}
	for (i = 1; i <= m; i++) {
		cin >> b[i].d >> b[i].s >> b[i].t;
	}
	int l = 1, r = m, mid, ans = 0;
	while (l <= r) {
		mid = (l + r) >> 1;
        //订单不满足,往前找第一个不满足的
		if (check(mid)) {
			r = mid - 1;
			ans = mid;
		}
		else {
			l = mid + 1;
		}
	}
    //说明订单一直被满足,ans没有被修改过
	if (ans == 0) {
		cout << 0 << endl;
		return 0;
	}
	else {
		cout << -1 << endl << ans;
	}
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值