漫谈整数二分

329 阅读4分钟

整数二分:

本文概述:

  • 文章以整数二分为背景,介绍了什么是整数二分;并通过具体示例介绍了二分的两个模板,同时指出二分的死循环难点怎么去避免。最后以模板题为例,给出了关键思路与完整代码。

什么是整数二分:

  • 有单调性则可二分,但可二分不一定有单调性

    • 对区间 [ l , r ] 内元素,存在性质 check() 使区间一分为二,左边元素不满足check(),右边元素满足;那么使用整数二分即可寻找红色区间右边界,或绿色区间左边界

      image-20230112130209198

整数二分算法思路:寻找红色区间的右边界

  1. 获取区间中点下标

    int mid = (l + r) >> 1;
    
  2. 判断 mid 下标对应元素是否满足目标区间性质

    bool flag = check_red(mid);
    
    • flagtrue 时:证明此时 mid 对应元素满足红色区间,且需要寻找红色区间的右边界,那么更新区间

      • 此时 mid 对应元素在红色区间内
      //此时区间[l,r] ---> [mid,r]
      if(flag == true){
          l = mid;
      }
      
    • flagflase 时:证明此时 mid 对应元素不满足红色区间,且需要寻找红色区间的右边界,那么更新区间

      • 此时 mid 对应元素在绿色区间
      //此时区间[l,r] ---> [l,mid - 1]
      if(flag == false){
          r = mid - 1;
      }
      
  1. 查询算法模板,决定是否需要在 mid 赋值时加一

    • 此时原区间 [ l , r ] 被划分为 [ l , mid - 1 ][ mid , r ],此时 mid 属于右区间,使用第二个模板;因此需要加一

      int mid = (l + r + 1) >> 1;
      
    • 为什么?

整数二分算法思路:寻找绿色区间的左边界

  1. 获取区间中点下标

    int mid = (l + r) >> 1;
    
  2. 判断 mid 下标对应元素是否满足目标区间性质

    bool flag = check_green(mid);
    
    • flagtrue 时:证明此时 mid 对应元素满足绿色区间,且需要寻找绿色区间的左边界,那么更新区间

      • 此时 mid 对应元素在绿色区间内
      //此时区间[l,r] ---> [l,mid]
      if(flag == true){
          r = mid;
      }
      
    • flagflase 时:证明此时 mid 对应元素不满足绿色区间,且需要寻找绿色区间的右边界,那么更新区间

      • 此时 mid 对应元素在红色区间
      //此时区间[l,r] ---> [mid + 1,r]
      if(flag == false){
          l = mid + 1;
      }
      
  1. 查询算法模板,决定是否需要在 mid 赋值时加一

    • 此时原区间 [ l , r ] 被划分为 [ l , mid ][ mid + 1 , r ],此时 mid 属于左区间,使用第一个模板;因此不需要加一

      int mid = (l + r) >> 1;
      
    • 为什么?

整数二分之死循环:为什么更新后的区间下标会影响mid 的赋值

  • 结论:

    • 当原区间 [ l , r ] 被划分为 [ l , mid ][ mid + 1 , r ]

      int mid = (l + r) >> 1;
      
    • 原区间 [ l , r ] 被划分为 [ l , mid - 1 ][ mid , r ]

      int mid = (l + r + 1) >> 1;
      
  • 为什么:

    • 因为 c++ 中的整数除法是下取整,考虑特例。

      //当 r = l + 1 时,
      int mid = (l + r) >> 1 = l
      
    • 当原区间 [ l , r ] 被划分为 [ l , mid - 1 ][ mid , r ]

      //此时,因为mid = l;
      //因此,区间更新[l,r] ---> [l,mid - 1]与[mid,r]
      //                  ---> [l,l - 1]与[l,r],陷入死循环      
      
  • 补上加一之后:

    • 因为 c++ 中的整数除法是下取整,考虑特例。

      //当 r = l + 1 时,
      int mid = (l + r + 1) >> 1 = r;
      
    • 当原区间 [ l , r ] 被划分为 [ l , mid - 1 ][ mid , r ]

      //此时,因为mid = l;
      //因此,区间更新[l,r] ---> [l,mid - 1]与[mid,r]
      //                  ---> [l,l]与[r,r],不满足while(l < r),跳出即可,不会陷入死循环       
      

整数二分在处理实际问题时的思考方式

  1. 为原区间元素选择 check(),并获取更新后的区间下标

  2. 根据更新后的区间下标,选择对应的算法模板

    • mid 位于更新后的左区间,则不需要加一

      //[l,r] 被划分为 [l,mid][mid + 1,r]
      
    • mid 位于更新后的右区间,则需要加一

      //[l,r] 被划分为 [l,mid - 1][mid,r]
      

算法模板一:

  • 适用条件:区间 [ l , r ] 被划分成 [ l , mid ][mid + 1,r] 时使用

  • 模板代码:

    int integer_binary_1(int l,int r)
    {
        while(l < r)
        {
            int mid = (l + r) >> 1;
            if(check(mid))  r = mid;//check(mid) 判断mid 是否满足某一性质
            else l = mid + 1;
        }
        return l;
    }
    

算法模板二:

  • 适用条件:区间 [ l , r ] 被划分成 [ l , mid - 1 ][ mid ,r] 时使用

  • 模板代码:

    int integer_binary_1(int l,int r)
    {
        while(l < r)
        {
            int mid = (l + r + 1) >> 1;//区别一:选择mid 时是否需要 + 1
            if(check(mid))  l = mid;//区别二:check(mid) 时的区间更新
            else l = mid - 1;
        }
        return l;
    }
    

样例展示:

  • 题目描述:

    image-20230112142248843

  • 输入输出展示:

    image-20230112142333605

关键思路:查找number 的左边界为例

  1. 先确定 mid 下标

    int mid = (l + r) >> 1;
    
  2. 根据目标,选择性质,一定要将区间一分为二

    if(nums[mid] <= number)
    
  3. 确定区间的更新方式,注意,一定要联系二分的目标

    • 目标:查找 number 出现的左边界
    • 建议画图理解:当 nums[mid] <= number 成立时,说明,目标所在区间为 [l,mid] 之间,因此,我们要更新 r;如果更新 l ,则错过了目标
    if(nums[mid] <= number)
    {
        r = mid;
    }else{
        l = mid + 1;
    }
    //[l,r] ---> [l,mid] 与 [mid + 1,r];
    
  4. 根据更新方式,选择对应模板

    • 此时 mid 被划分在了左区间,因此不需要在初始赋值时进行加一操作

完整代码:c++

#include<iostream>using namespace std;
​
const int N = 1e6 + 10;
​
int nums[N];
​
int binary_start(int l,int r,int number)//寻找num 出现的左边界
{
    while(l < r)
    {
        int mid = (l + r) >> 1;
        
        if(nums[mid] >= number)
        {
            r = mid;
        }else{
            l = mid + 1;
        }
    }
    
    return l;
}
​
int binary_end(int l,int r,int number)//寻找num 出现的右边界
{
    while(l < r)
    {
        int mid = (l + r + 1) >> 1;
        {
            if(nums[mid] <= number)
            {
                l = mid;
            }else{
                r = mid - 1;
            }
        }
    }
    
    return l;
}
​
int main()
{
    //处理IO
    int n,q;
    scanf("%d%d",&n,&q);
    for(int i = 0;i < n;i++)    scanf("%d",&nums[i]);
    
    while(q--)
    {
        int number;
        scanf("%d",&number);//读入每次询问
        
        int start = binary_start(0,n - 1,number);//寻找number 出现的左边界
        if(nums[start] != number)//当number 左边界不存在即待查询的数number 不在原数组中
        {
            cout<< "-1 -1" << endl;
            continue;
        }else{
            cout << start << ' ';
            int end = binary_end(0,n - 1,number);//寻找num 出现的右边界
            cout << end << endl;//若左边界存在,那么有边界一定存在(当number 仅出现一次,左右边界相同 )
        }
        
    }
}

完整代码:Java

import java.io.BufferedInputStream;
import java.util.Scanner;
​
public class Main {
​
    public int binary_start(int l, int r,int number,int[] nums){//寻找number 出现的左边界
        while (l < r){
            int mid = (l + r) >> 1;
            if(nums[mid] >= number){
                r = mid;
            }else{
                l = mid + 1;
            }
        }
        return l;
    }
​
    public int binary_end(int l,int r,int number,int[] nums){//寻找number 出现的有边界
        while (l < r){
            int mid = (l + r + 1) >> 1;
            if(nums[mid] <= number){
                l = mid;
            }else{
                r = mid - 1;
            }
        }
        return l;
    }
​
    public static void main(String[] args) {
        Main ma = new Main();
​
        //处理IO
        Scanner sc = new Scanner(new BufferedInputStream(System.in));
​
        int n,q;
        n = sc.nextInt();
        q = sc.nextInt();
        int nums[] = new int[n];
        for (int i = 0;i < n;i++){
            nums[i] = sc.nextInt();
        }
​
        while (q-- > 0){//处理q 次询问
            int number = sc.nextInt();
            int start = ma.binary_start(0,n - 1,number,nums);
            if(nums[start] != number){
                System.out.println("-1 -1");
                continue;
            }else{
                System.out.print(start + " ");
                int end = ma.binary_end(0,n - 1,number,nums);
                System.out.println(end);
            }
        }
    }
}
​