zoukankan      html  css  js  c++  java
  • 二分查找

    二分查找

    1.普通的二分查找 - 查找某数在数组里的坐标,如果不存在返回 -1

    • 该方式不稳定(可能找到的是重复元素的几个当中不确定的一个)
     int binarySearch(vector<int> nums, int target) {
     int left = 0;
     int right = nums.size() - 1;
         while (left <= right) {
             int mid = left + (right - left) / 2;
             if (nums[mid] == target) {
                 return mid;
             } 
             else if (nums[mid] < target) {
                 left = mid + 1;
             } 
             else if (nums[mid] > target) {
                 right = mid - 1;
             }
         } 
         return -1;
     }
    

    2.1.查找小于target的数的坐标

     int binarySearch(vector<int> nums, int target) {
         int right = nums.size();
         if (right == 0)
             return -1;
         int left = 0;
         while (left < right) {
             int mid = left + (right - left) / 2;
             if (nums[mid] == target) {
                 right = mid;
             }
             else if (nums[mid] < target) {
                 left = mid + 1;
             } 
             else if (nums[mid] > target) {
                 right = mid;
             }
         } 
         return left;
    

    这里return left的判断条件在于right = mid(也就是取得等号的时候的条件),最终终止循环的条件会是left == right,所以实际上我们可以得到可能出现在哪个位置的target(也就是说,我们没有漏掉任何一个区间)。

    3.1 .查找大于target的数的坐标

     int binarySearch(vector<int> nums, int target) {
         int right = nums.size();
         if (right == 0)
             return -1;
         int left = 0;
         while (left < right) {
             int mid = left + (right - left) / 2;
             if (nums[mid] == target) {
                 left = mid + 1;//等于的时候left要复制加一,所以最终返回**left-1**
             } 
             else if (nums[mid] < target) {
                 left = mid + 1;
             } 
             else if (nums[mid] > target) {
                 right = mid;
             }
         } 
         return left-1;
     }
    

    这里所要返回的left-1是因为我们赋值的时候是left=mid+1也就是mid=left-1,所以最后可能得到的target的位置不可能是left,只可能是left-1,也就是mid的位置。

    2.2.根据「小于target的数的坐标」改进的稳定的二分查找

     int binarySearch(vector<int> nums, int target) {
         int right = nums.size();
         if (right == 0)
             return -1;
         int left = 0;
         while (left < right) {
             int mid = left + (right - left) / 2;
             if (nums[mid] == target) {
                 right = mid;
             } 
             else if (nums[mid] < target) {
                 left = mid + 1;
             } 
             else if (nums[mid] > target) {
                 right = mid;
             }
         } 
         return (nums[left] == target) ? left : -1;//区间终止条件**left==right**,所以要判断最后的left位置是否合理。
     }
    

    3.2. 根据「大于target的数的坐标」改进的稳定的二分查找

     int binarySearch(vector<int> nums, int target) {
         int right = nums.size();
         if (right == 0)
             return -1;
         int left = 0;
         while (left < right) {
             int mid = left + (right - left) / 2;
             if (nums[mid] == target) {
                 left = mid+1;
             } 
             else if (nums[mid] < target) {
                 left = mid + 1;
             } 
             else if (nums[mid] > target) {
                 right = mid;
             }
         } 
         return (nums[left-1] == target) ? left-1 : -1;//区间终止条件**left==right**,所以要判断最后的left-1位置是否合理。
     }
    

    4. 注意

    • left + right 可能会超出 INT_MAX,为了防止溢出:c int mid = (left + right) / 2 修改为c int mid = left + (right - left) / 2;

    • right 的初始化

      • right = nums.size()
      • 用于搜索小于或者大于target的一个范围
      • 搜索区间为 [left, right)
      • while(left < right)
      • 结束条件为 left == right
      • 结束区间为[left, right],非空有一个值
      • left = mid + 1;
      • right = mid;

    • right = nums.size() - 1

      • 用于搜索target的坐标

      • 搜索区间为 [left, right]

      • while(left <= right)

      • 结束条件为left == right + 1

      • 结束区间为 [right + 1, right],区间为空

      • left = mid + 1;

      • right = mid - 1;

      • [x] xiaowuga

      二分搜索:各种二分

      • 由于常年二分写成死循环,所以是时候有必要总结一下二分搜索了,这里声明一下本人的二分风格是左闭右开也就是[L,R)。
      • 这里就不解释什么是二分搜索了,这里将会介绍4种二分搜索,和二分搜索常用来解决的最小值最大化或者最大值最小化的问题,我们都知道使用二分的最基本条件是,我们二分的序列需要有单调性,这里的序列是广义性如:
      1. 一个排好序的数组;
      2. 一个区间[L,R);
      3. 其他(暂时想不到)。
      4. 所以下面介绍的时候会用v来代表我们二分的目标,用第一个大于v,第一个大于等于v,最后一个小于v,最后一个小于等于v来描述,这里可以看到我即将要介绍的4种二分搜索。

      1.第一个大于等于v

      这就是我们常说的lower_bound()了,这是系统里面自带的库函数,在数组或者一个vector容器中二分的时候,也就是不是必须手写二分的时候首推使用这个,优点代码少,稳定(建议少装逼,动不动手写二分)。这里我们来介绍lower_bound()的使用方式。

      首先是这个函数原型:

      ForwardIterator lower_bound (ForwardIterator first, ForwardIterator last,const T& val, Compare comp)
      

      其中first代表左边界迭代器,last代表右边界迭代器(注意左闭右开),val代表你要搜索的值,comp代表排序规则(这个参数在你对非结构体数组二分的时候并不需要,有默认规则)

      实例:

      int a[100]={1,2,3,3,4,5,5,6,8,9,22},n;
          while(cin>>n){
              int p=lower_bound(a,a+11,n)-a;
              //如果a是vector,那么lower(a.begin(),a.end(),v)-a.begin();
              //你也可以在指定在[L,R)区间内二分lower_bound(a.begin()+L,a.begin()+R,v)-a.begin(),数组也是同理的
              cout<<p<<endl;//这里输出的是第一个大于等于n的数的下标
          }
      

      当对结构体数组进行二分搜索时(我们可以在这里继续输入上面代码的初始化的数据)

      #include<bits/stdc++.h>
      using namespace std;
      struct node{
          int x;
      };
      int cmp(node a,node b){
          return a.x<b.x;//注意这里不可以a.x<=b.x不然lower_bound就变成upper_bound了
      }
      int main(){
          int n;cin>>n;
          vector<node>a(n); 
          node b;
          for(int i=0;i<n;i++) cin>>a[i].x;
          while(cin>>b.x){
              int p=lower_bound(a.begin(),a.end(),b,cmp)-a.begin();
              cout<<p<<endl;//输出还是下标
          }
          return 0;
      }
      

      这里我们介绍的是当数组是升序的时候的情况,如果数组是降序的,我们则需要重新定义排序规则,我们这里在使用lower_bound()就是寻找第一个小于等于v的下标。

      学会了如何使用库函数,现在我们来学习一下如何手写一个lower_bound(),我们知道二分有一个左边界L和右边界R,我们定义[L,R)内的下标都小于v,我们假设L为当前区间的答案,R为当前区间的实际答案(因为R是第一个大于等于v的下标),我们每次二分的实际上是为了让L和R不断靠近,所以当L==R的时候,我们假设的答案等于实际的答案,那么就结束循环了,返回答案L。

      #include<bits/stdc++.h>
      using namespace std;
      int main(){
          int a[100]={1,2,3,3,4,5,5,6,8,9,22},v;
          while(cin>>v){
              int L=0,R=11;
              while(L<R){
                  int M=(L+R)/2;
                  if(a[M]>=v) R=M;
                  else L=M+1;
              }
              cout<<L<<endl;
          }    
          return 0;
      }
      
      

      注:1.当a[M]>=n时,由于R是第一个大于等于v下标,那么R最大只能是m

      2.当a[M]<n时,说明[M,R)区间内的下标都是小于v的,L作为最后的答案最小只能是M+1

      2.第一个大于v

      这就是我们常说的upper_bound()了,这是系统里面自带的库函数,这里我们来介绍upper_bound()的使用方式,和lower_bound()在可以使用的时候推荐使用。

      首先函数原型:

      ForwardIterator upper_bound (ForwardIterator first, ForwardIterator last,const T& val, Compare comp);
      

      其中first代表左边界迭代器,last代表右边界迭代器(注意左闭右开),val代表你要搜索的值,comp代表排序规则(这个参数在你对非结构体数组二分的时候并不需要,有默认规则)

      实例:

      int a[100]={1,2,3,3,4,5,5,6,8,9,22},n;
      while(cin>>n){
          int p=upper_bound(a,a+11,n)-a;
          //如果a是vector,那么upper_bound(a.begin(),a.end(),v)-a.begin();
          //你也可以在指定在[L,R)区间内二分upper_bound(a.begin()+L,a.begin()+R,v)-a.begin(),数组也是同理的
          cout<<p<<endl;//这里输出的是第一个大于等于n的数的下标
      }
      

      当对结构体数组进行二分搜索时(我们可以在这里继续输入上面代码的初始化的数据)

      #include<bits/stdc++.h>
      using namespace std;
      struct node{
          int x;
      };
      int cmp(node a,node b){
          return a.x<=b.x;//注意这里不可以a.x<=b.x不然upper_bound就变成lower_bound了
      }
      int main(){
          int n;cin>>n;
          vector<node>a(n); 
          node b;
          for(int i=0;i<n;i++) cin>>a[i].x;
          while(cin>>b.x){
              int p=upper_bound(a.begin(),a.end(),b,cmp)-a.begin();
              cout<<p<<endl;//输出还是下标
          }
          return 0;
      }
      

      这里我们介绍的是当数组是升序的时候的情况,如果数组是降序的,我们则需要重新定义排序规则,我们这里在使用upper_bound()就是寻找第一个小于v的下标。

      学会了如何使用库函数,现在我们来学习一下如何手写一个upper_bound(),同样的,我们定义[L,R)内的下标都小于等于v,我们假设L为当前区间的答案,R为当前区间的实际答案(因为R是第一个大于v的下标),我们每次二分的实际上是为了让L和R不断靠近,所以当L==R的时候,我们假设的答案等于实际的答案,那么就结束循环了,返回答案L。

      #include<bits/stdc++.h>
      using namespace std;
      int main(){
          int a[100]={1,2,3,3,4,5,5,6,8,9,22},v;
          while(cin>>v){
              int L=0,R=11;
              while(L<R){
                  int M=(L+R)/2;
                  if(a[M]>v) R=M;
                  else L=M+1;
              }
              cout<<L<<endl;
          }    
          return 0;
      }
      
      

      注:1.当a[M]>n时,由于R是第一个大于v下标,那么R最大只能是M

      2.当a[M]<=n时,说明[M,R)区间内的下标都是小于等于v的,L作为最后的答案最小只能是M+1

      3.最后一个小于等于v

      上面说过了,当数组为降序的,使用lower_bound就是返回第一个小于等于下标,若一开始数组是升续的时候,那么应该先reverse一下,再用lower_bound返回下标p,则在原数组中的下标为n-p-1(假设数组有n个元素)。

      这里来介绍一下如何在如果手写一个last_less_equal()。和lower_bound二分区间[L,R)左闭右开不同,last_less_equal()的二分区间为(L,R]右闭左开。

      同样的,我们定义(L,R]内的下标都大于v,我们假设R为当前区间的答案,L为当前区间的实际答案(因为L是最后一个小于等于v的下标),我们每次二分的实际上是为了让L和R不断靠近,所以当L==R的时候,我们假设的答案等于实际的答案,那么就结束循环了,返回答案L。

      #include<bits/stdc++.h>
      using namespace std;
      int main(){
          int a[100]={1,2,3,3,4,5,5,6,8,9,22},v;
          while(cin>>v){
              int L=-1,R=10;
              while(L<R){
                  int M=(L+R+1)/2;
                  if(a[M]<=v) L=M;
                  else R=M-1;
              }
              cout<<L<<endl;
          }    
          return 0;
      }
      

      注:1.当a[M]<=n时,由于L是最后一个小于等于v下标,那么L最小只能是M。

      2.当a[M]>n时,说明(L,M]区间内的下标都是大于v的,R作为最后的答案最大只能是M-1。

      4.最后一个小于v

      上面说过了,当数组为降序的,使用upper_bound就是返回第一个大于下标,若一开始数组是升续的时候,那么应该先reverse一下,再用upper_bound返回下标p,则在原数组中的下标为n-p-1(假设数组有n个元素)。

      这里来介绍一下如何在如果手写一个last_less()。和upper_bound二分区间[L,R)左闭右开不同,last_less_equal()的二分区间为(L,R]右闭左开。

      同样的,我们定义(L,R]内的下标都大于等于v,我们假设R为当前区间的答案,L为当前区间的实际答案(因为L是最后一个小于v的下标),我们每次二分的实际上是为了让L和R不断靠近,所以当L==R的时候,我们假设的答案等于实际的答案,那么就结束循环了,返回答案L。

      #include<bits/stdc++.h>
      using namespace std;
      int main(){
          int a[100]={1,2,3,3,4,5,5,6,8,9,22},v;
          while(cin>>v){
              int L=-1,R=10;
              while(L<R){
                  int M=(L+R+1)/2;
                  if(a[M]<v) L=M;
                  else R=M-1;
              }
              cout<<L<<endl;
          }    
          return 0;
      }
      

      注:1.当a[M]<n时,由于L是最后一个小于v下标,那么L最小只能是M。

      2.当a[M]>=n时,说明(L,M]区间内的下标都是大于等于v的,R作为最后的答案最大只能是M-1。

      我们发现lower_bound()和upper_bound()的M=(L+R)/2,而last_less()和last_less_equal()的M=(L+R+1)/2,(L+R)/2和(L+R+1)/2的区别在于前者是向下取整,后者是向上取整,这和我们定义L或者R是实际的答案有关。

      有一类常见问题叫做最小值最大化或者最大值最小化。这类问题一般是用二分搜索来解决。

      • 首先二分搜索解决的问题必须具备单调性这个性质,这是使用二分搜索的必要条件,我们分析两个问题。
      1. **最小值最大化:****我们假设x为最大的最小值,那么x-1是满足条件的,但他并不满足最大,x+1是不满足条件的,假设我们左边界是L,右边界是R,我们二分一个答案ans,ans为最后一个满足条件的数,我们是不是可以类比二分搜索(一)中的last_less_equal()或者last_less()这个问题和这两者是差不多的。可以先阅读我的另一篇博文:二分搜索(一)——各种二分
      2. 最大值最小化:我们假设x为最小的最大值,那么x-1是不满足条件的,x+1是满足条件的,但他不满足最小,假设我们左边界是L,右边界是R,我们二分一个答案ans,ans为第一个满足条件的数,我们是不是可以类比二分搜索(一)中的lower_bound()或者upper_bound()这个问题和这两者是差不多的。
      3. 所以综上所述并根据我在二分搜索(一)——各种二分中的描述:最小值最大化的二分区间是右闭左开(L,R],每次二分的中心为M=(L+R+1)/2;最大值最小化的二分区间是左闭右开,[L,R),每次二分的中心为M=(L+R)/2。

      例题1:LA3971-3971——Assemble

      题目意思:你有b块钱,想要组装一台电脑。给出n个配件格子的种类,品质因子和价格,要求每种类型的配件各买一个,总价格不超过b,且品质最差的配件的品质因子尽量大。

      思路:这很明显是一个最小值最大化的问题,这道题还用到map对物品按名称进行分类,注意多组输入,要对上一组的数据进行清空,我们可以看出二分边界L=-1,R=maxq(所有商品中品质因子的最大值。),也就是右闭左开区间(L,R],我们搜索最后一个满足条件的ans值,具体看代码吧。

      代码:

      #include<bits/stdc++.h>
      using namespace std;
      const int N=1000+7;
      map<string,int>mp;
      struct node{
          int p,q;    
      };
      vector<node>a[N];
      int cnt=0,n,b;
      int check(int M){
          long long sum=0;
          for(int i=0;i<cnt;i++){
              int minn=b+10;
              for(int j=0;j<(int)a[i].size();j++){
                  if(a[i][j].q>=M){
                      minn=min(minn,a[i][j].p);
                  }
              }
              sum+=minn;
              if(sum>b) return 0;
          }
          return 1;
      }
      int main(){
          int T;cin>>T;
          while(T--){
              cin>>n>>b;    
              for(int i=0;i<cnt;i++) a[i].clear();
              cnt=0;
              int L=-1,R=0;
              for(int i=0;i<n;i++){
                  string type,name;
                  int p,q;
                  cin>>type>>name>>p>>q;
                  if(mp.count(type)==0){
                      mp[type]=cnt++;
                  }
                  R=max(R,q);
                  a[mp[type]].push_back({p,q});
              }
              while(L<R){
                  int M=(L+R+1)/2;
                  if(check(M)) L=M;
                  else R=M-1;
              }
              cout<<R<<endl;
          }
          return 0;
      }
      

      例题2:openjudge-2456——Aggressive cows

      题目意思:农民约翰有用C只牛,然后他有N个隔间,每个隔间都有自己的坐标位置(一维的)pos,如何安排把牛安排进隔间才能使,所有牛之间距离的最小值最大,我们不需要求这个分配方案,我们只需要求这个最小距离的最大值,很裸的最小值最大化。

      思路:直接看代码吧

      代码:

      #include<bits/stdc++.h>
      using namespace std;
      typedef long long LL;
      const int N=1e5+7;
      LL n,c,a[N];
      int check(int M){
          LL t=c-1,pre=0;
          for(int i=1;i<n;i++){
              if(a[i]-a[pre]>=M){
                  t--;
                  pre=i;
              }
              if(t==0) break;
          }
          return t==0;    
      }
      int main(){
          cin>>n>>c;    
          long long minn=0x3f3f3f3f,maxx=-0x3f3f3f3f;
          for(int i=0;i<n;i++){
              cin>>a[i];
              minn=min(a[i],minn); maxx=max(a[i],maxx);
          }
          sort(a,a+n);
          int L=0,R=maxx-minn;    
          while(L<R){
              int M=(L+R+1)/2;
              if(check(M)) L=M;
              else R=M-1;
          }
          cout<<R<<endl;
          return 0;
      }
      

      例题3:openjudge-4135——Monthly Expense

      题目意思:共n个月,给出每个月的开销.将n个月划分成m个时间段,求m个时间段中开销最大的时间段的最小开销值。

      思路:最大值最小化,直接看代码吧

      代码:

      #include<bits/stdc++.h>
      using namespace std;
      int n,m;
      vector<int>a;
      int check(int M){
          int ct=0,now=0;
          for(int i=0;i<n;i++){
              if(a[i]>M) return 0;
              if(now+a[i]>M){
                  ct++;
                  now=0;    
              }    
              now+=a[i];
          }
          return ct<m;
      }
      int main(){
          cin>>n>>m;
          a.resize(n);
          int R=0,L=0;
          for(int i=0;i<n;i++){
              cin>>a[i];
              R+=a[i];
              L=max(L,a[i]);
          }
          R++;    
          while(L<R){
              int M=(L+R)/2;
              if(check(M)) R=M;
              else L=M+1;
          }
          cout<<L<<endl;
          return 0;
      }
      
    抬起头,永远年轻,永远热泪盈眶!
  • 相关阅读:
    十三.基础邮件服务、parted分区工具、交换分区、链路聚合
    十二.虚拟Web主机
    十一.简单MariaDB数据库的管理
    十.iSCSI网络磁盘
    九.配置SMB共享(Samba共享)
    八.防火墙相关操作
    bzoj3132
    bzoj4753
    codeforces round #418 div 2
    ural1519
  • 原文地址:https://www.cnblogs.com/marvin-Hua-manlou/p/14271406.html
Copyright © 2011-2022 走看看