【算法修炼】二分模板+常见题型总结,搞懂“二分”这一篇就够了

    // 传入的数组必须是升序
    // 递归实现二分查找
    static int binarySearch(int[] arr, int low, int high, int key){
        if(low > high){
            // 没找到
            return -1;
        }
        int mid = (high + low) / 2;
        int midVal = arr[mid];
        if(midVal < key){
            return binarySearch(arr, mid+1, high, key);
        }else if(midVal > key){
            return binarySearch(arr, low, mid-1, key);
        }else{
            // 找到了
            return mid;
        }
    }
    // 迭代实现二分查找
    // 注意这个二分查找,找到的是最后一个满足条件的数,而不是第一个!!
    static int binarySearch1(int[] arr, int key){
        int begin = 0;
        int end = arr.length - 1;
        int mid;
        while(begin <= end){
            mid = (begin + end) / 2;
            if(arr[mid] == key){
                return mid;
            }else if(arr[mid] > key){
                end = mid - 1;
            }else if(arr[mid] < key){
                begin = mid + 1;
            }
        }
        // 没找到
        return -1;
    }
    // 迭代实现二分查找:第一个大于等于key的数下标,找不到就返回数组最后一个元素的下一个下标
    static int lowerbound(int[] arr, int key){
        int begin = 0;
        int end = arr.length;
        int mid;
        while(begin < end){
            mid = (begin + end) / 2;
            if(arr[mid] >= key){
                end = mid;
            }else{
                begin = mid + 1;
            }
        }
        return begin;
    }
    // 迭代实现二分查找:第一个大于key的数的下标,找不到就返回数组最后一个元素的下一个下标
    // 也就是数组长度
    static int upperbound(int[] arr, int key){
        int begin = 0;
        int end = arr.length;
        int mid;
        while(begin < end){
            mid = (begin + end) / 2;
            if(arr[mid] > key){
                end = mid;
            }else{
                begin = mid + 1;
            }
        }
        return begin;
    }

1、为防止溢出,mid = (begin + end) / 2可以改写为mid = begin + (end - begin) / 2。

2、为什么lower_bound、upper_bound的end要=n?二分初始区间为[0,n]
考虑到待查询的元素值可能大于数组中的所有元素,此时就可以让它返回n,方便利用二分完成其它操作。

3、lower和upper_bound本质上都是在解决:寻找有序序列中第一个满足某条件的元素的位置,大部分二分问题都能直接归结于这个问题。lower的某条件是:大于等于某个值,upper的某条件是:大于某个值。并且这个条件,在排序后,是先从左到右不满足,然后再满足的!
对于寻找第一个大于等于、第一个大于target的下标,都是返回left(begin)。

常见题型

一定要注意题目中是否给出有序、非降序等词句,如果没有一定要排序!因为有些题目没有排序,但它给你的输入实例是排序的,会被误导。

1、就是直接套公式,不需要任何变形

2、给定值最接近的元素

在这里插入图片描述
https://nanti.jisuanke.com/t/T1156

关键在于如何理解:最接近相应给定值的元素值,并且有多个值满足时,要输出最小的一个。
思路:可以用lower_bound找到第一个大于等于给定值的下标,再对这个下标对应的元素值进行判断。
1、如果下标 = 数组长度,说明没有一个满足大于等于条件的元素,那么直接返回最后一个元素。
2、如果下标 = 0,直接返回第一个元素。
3、比较arr[res - 1] 与 arr[res],分别和给定值做差,比较差的绝对值,选用差更小的下标。

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        int n = scan.nextInt();
        int[] arr = new int[n];
        for (int i = 0; i < n; i++) {
            arr[i] = scan.nextInt();
        }
        int m = scan.nextInt();
        for (int i = 0; i < m; i++) {
            int tmp;
            tmp = scan.nextInt();
            int res = lowerbound(arr, tmp);
            // 主要是判断条件
            if(res == 0){
                System.out.println(arr[0]);
            }else{
                if(res == arr.length){
                    res = res - 1;
                }else if(Math.abs(arr[res - 1] - tmp) <= Math.abs(arr[res] - tmp)){ 
                    res = res - 1;
                }
                System.out.println(arr[res]);
            }
        }
    }
    static int lowerbound(int[] arr, int tmp){
        int begin = 0;
        int end = arr.length;
        while(begin <= end){
            int mid = begin + (end - begin) / 2;
            if(arr[mid] >= tmp){
                end = mid;
            }else{
                begin = mid + 1;
            }
        }
        return begin;
    }
}

3、等于给定值的元素个数

在这里插入图片描述
https://nanti.jisuanke.com/t/T1563

主要需要解决:如何统计等于x的个数,可以利用lower_bound和upper_bound的特性,一个找第一个大于等于的元素,一个找第一个大于的元素,直接利用下标关系相减就可以知道个数,当然还要判断不存在该数的情况。

import java.util.Arrays;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        int n, m;
        Scanner scan = new Scanner(System.in);
        n = scan.nextInt();
        m = scan.nextInt();
        int[] arr = new int[n];
        for (int i = 0; i < n; i++) {
            arr[i] = scan.nextInt();
        }
        Arrays.sort(arr);
        for (int i = 0; i < m; i++) {
            int tmp = scan.nextInt();
            int res = lowerbound(arr, tmp);
            int res1 = upperbound(arr, tmp);
            // 用lowerbound判断元素是否存在
            if(res == arr.length || arr[res] != tmp){
                System.out.println(0);
            }else{
                System.out.println(res1 - res);
            }
        }
    }
    static int lowerbound(int[] arr, int tmp){
        int begin = 0;
        int end = arr.length;
        int mid;
        while(begin < end){
            mid = begin + (end - begin) / 2;
            if (arr[mid] >= tmp){
                end = mid;
            }else{
                begin = mid + 1;
            }
        }
        return begin;
    }
    static int upperbound(int[] arr, int tmp){
        int begin = 0;
        int end= arr.length;
        int mid;
        while(begin < end){
            mid = begin + (end - begin) / 2;
            if (arr[mid] > tmp){
                end = mid;
            }else{
                begin = mid + 1;
            }
        }
        return begin;
    }
}

4、小于等于给定值的最大值

在这里插入图片描述
https://nanti.jisuanke.com/t/T1563

转变思路,小于等于x的最大值,直接求是不好求的,可以利用lower_bound找到的下标进行求解,如果下标对应的值=x,那就直接打印;如果不等于,那下标-1就是答案。当然也别忘了对特殊情况的处理,res=0、res=length。

import java.util.Arrays;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        int n, m;
        Scanner scan = new Scanner(System.in);
        n = scan.nextInt();
        m = scan.nextInt();
        int[] arr = new int[n];
        for (int i = 0; i < n; i++) {
            arr[i] = scan.nextInt();
        }
        Arrays.sort(arr);
        for (int i = 0; i < m; i++) {
            int tmp = scan.nextInt();
            int res = binarysearch(arr, tmp);
            if(res == arr.length){
                System.out.println(arr[res - 1]);
            }else{
                if(arr[res] == tmp){
                    System.out.println(arr[res]);
                }else{
                    if(res == 0){
                        System.out.println(-1);
                    }else{
                        System.out.println(arr[res - 1]);
                    }
                }
            }
        }
    }
    static int binarysearch(int[] arr, int tmp){
        int begin = 0;
        int end = arr.length;
        int mid;
        while(begin < end){
            mid = begin + (end - begin) / 2;
            if(arr[mid] >= tmp){
                end = mid;
            }else{
                begin = mid + 1;
            }
        }
        return begin;
    }

}

5、小于给定值的最大值

在这里插入图片描述
https://nanti.jisuanke.com/t/T1556

在4的基础上修改即可。

6、分派(浮点数二分模拟)

在这里插入图片描述
在这里插入图片描述
https://nanti.jisuanke.com/t/T1157

这类题型就是利用二分查找快速的特点,寻找满足某个方程的解,这个解按题目需求,可能是整数也可能是浮点数,while循环的条件变成了begin和end之间的差值,也可以说是精度EPS。

while(end - begin > EPS){
     double mid = begin + (end - begin) / 2;
     if(某个条件){
         begin = mid;
     }else{
         end = mid ;
     }
}

注意,在用二分进行方程未知数求解时(或模拟求值时),begin和mid的更新都是=mid,while判断条件则是begin和end之间的精度。

本题中,我们把派的体积作为二分对象,begin=0,end=最大派的体积,在while循环中判断当前派的体积是否够分(不要忘了自己),如果够就再进一步进行逼近(派体积最大化),最后结果就是begin。

import java.util.Scanner;

public class Main {
    public static int N, F;
    public static double[] r = new double[10001];
    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        N = scan.nextInt();
        F = scan.nextInt();
        double begin = 0;
        double end = 0;
        for (int i = 0; i < N; i++) {
            r[i] = scan.nextDouble();
            // 找到最大的派大小作为二分右边界
            r[i] = Math.PI * r[i] * r[i];
            end = Math.max(end, r[i]);
        }
        double EPS = 0.00001;
        while(end - begin > EPS){
            double mid = begin + (end - begin) / 2;
            if(check(mid) == 1){
                // 当前情况下满足划分要求,尝试最大化每个人的派体积
                begin = mid;
            }else{
                end = mid ;
            }
        }
        System.out.printf("%.3f", begin);
    }
    static int check(double n){
        int num = 0;
        for (int i = 0; i < N; i++) {
            num += r[i] / n;
            if(num >= F + 1){
                return 1;
            }
        }
        return -1;
    }
}

7、计算根号2的近似值

我们可以利用 x * x 函数来求解,第一它是单调递增的(满足二分要求),第二根号2 * 根号2=2,方便判断。

import java.util.Arrays;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        double begin = 1;
        double end = 2;
        double eps = 1e-5;
        double mid = 0;
        while(end - begin > eps){
            mid = begin + (end - begin) / 2;
            if(f(mid) > 2){
                end = mid;
            }else{
                begin = mid;
            }
        }
        // begin可以最大可能的逼近根号2
        System.out.println(begin);
    }
    static double f(double x){
        return x * x;
    }
}

7、x的平方根(简单)(整数)

在这里插入图片描述
直接模拟,注意使用x / mid == mid,代替mid * mid == x,防止mid * mid溢出。

class Solution {
    public int mySqrt(int x) {
        if (x == 0 || x == 1) return x;
        int left = 0;
        int right = x;
        int mid;
        while (left <= right) {
            mid = left + (right - left) / 2;
            if (mid == x / mid) {
                return mid;
            } else if (mid < x / mid) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return left - 1;
    }
}

7、x的平方根(小数)

如果是小数模拟,就需要精度eps。

class Solution {
    public double mySqrt(int x) {
        double left = 0;
        double right = x;
        double mid = 0;
        // 模拟精度
        double eps = 0.000001;
        while (right - left > eps) {
            mid = left + (right - left) / 2;
            if (mid == x / mid) {
                return mid;
            } else if (mid < x / mid) {
                left = mid;
            } else {
                right = mid;
            }
        }
        return left;
    }
}

8、银行贷款(浮点数方程求解问题)

在这里插入图片描述
在这里插入图片描述
https://nanti.jisuanke.com/t/T1871

还是用上面的模板,只不过求解的方程变了,注意这种题一般都不会出现值恰好等于的情况,所以只用判断值大于还是小于目标值来逼近方程的解。

import java.util.Arrays;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        int x, y, n;
        Scanner scan = new Scanner(System.in);
        x = scan.nextInt();
        y = scan.nextInt();
        n = scan.nextInt();
        double begin = 0.00001;
        double end = 10;
        double eps = 0.00001;
        double mid;
        while(end - begin > eps){
            mid = begin + (end - begin) / 2;
            double sum = 0.0;
            for (int i = 1; i <= n ; i++) {
                sum += y * 1.0 / Math.pow((mid + 1), i);
            }
            // 值大了要把分母也变大
            if (sum > x){
                begin = mid;
            }else{
                end = mid;
            }
        }
        System.out.printf("%.1f", begin * 100);
    }
}

9、切绳子(满足给定条件的浮点数二分)

在这里插入图片描述
在这里插入图片描述
https://nanti.jisuanke.com/t/T1883

用上面同样的模板,判断条件为当前二分的绳子长度能否把n条绳子划分出m条,如果可以我们让begin = mid,最大化可能的绳子长度。

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        int n, m;
        Scanner scan = new Scanner(System.in);
        n = scan.nextInt();
        m = scan.nextInt();
        // n条绳子,分出m条长度相同的绳子,这m条最长可以是多长
        double[] arr = new double[n];
        double begin = 0;
        double end = 0;
        for (int i = 0; i < n; i++) {
            arr[i] = scan.nextDouble();
            end = Math.max(arr[i], end);
        }
        double mid;
        double eps = 0.00000001;
        while(end - begin > eps){
            mid = begin + (end - begin) / 2;
            int num = 0;
            for (int i = 0; i < n; i++) {
                // 当前绳子长度能否分够m根
                num += arr[i] / mid;
            }
            if(num >= m){
                // 能够分够,再最大化每根绳子长度
                begin = mid;
            }else{
                // 不能分够,说明绳子长度大了
                end = mid;
            }
        }
        System.out.printf("%.6f", begin);
    }
}

※10、丢瓶盖(满足给定条件的二分整数查找)

在这里插入图片描述
https://nanti.jisuanke.com/t/T1878

这道题跟之前的浮点数模拟不同,因为二分的对象是整数,并且条件是可能等于的,这里使用最普通的二分模板,用ans记录最大的距离值。

import java.util.Arrays;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        int a, b;
        Scanner scan = new Scanner(System.in);
        a = scan.nextInt();
        b = scan.nextInt();
        int[] arr = new int[a];
        for (int i = 0; i < a; i++) {
            arr[i] = scan.nextInt();
        }
        Arrays.sort(arr);
        // 最大距离的最小情况=0
        int begin = 0;
        // 最大距离的最大情况=末尾-首
        int end = arr[a - 1] - arr[0];
        int mid = 0;
        // 对最近的两个瓶盖的最大距离进行二分
        int ans = 0;
        while(begin <= end){
            mid = begin + (end - begin) / 2;
            int cnt = 1;
            int tmp = arr[0];
            // 看当前二分对象能否满足至少B个瓶盖
            for (int i = 1; i < a; i++) {
                if((arr[i] - tmp) >= mid){
                    cnt++;
                    tmp = arr[i];
                }
            }
            if(cnt >= b){
                begin = mid + 1;
                ans = mid;
            }else{
                end = mid - 1;
            }
        }
        System.out.println(ans);
    }
}
如果题目是使用二分进行浮点数、整数找满足某条件的值,需要分别使用不同的模板进行求解。

11、和为给定数(定一找一)

在这里插入图片描述
在这里插入图片描述
https://nanti.jisuanke.com/t/T1158

如果用两个循环遍历,铁定超时,所以我们可以固定一个值,再用二分去找另一半,看能否找到。
注意:因为使用二分前要进行排序,题目只需要输出较小的数组成的数对,只用判断<=m/2即可,避免重复查找。

import java.util.Arrays;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        int n;
        Scanner scan = new Scanner(System.in);
        n = scan.nextInt();
        long[] arr = new long[n];
        for (int i = 0; i < n; i++) {
            arr[i] = scan.nextLong();
        }
        Arrays.sort(arr);
        long m;
        m = scan.nextLong();
        int res = -1;
        for (int i = 0; i < n; i++) {
            // 关键,避免重复查找
            if(arr[i] <= m / 2){
                res = binarysearch(arr, m - arr[i]);
                if(res == -1){
                    continue;
                }else{
                    System.out.printf("%d %d", arr[i], m - arr[i]);
                    break;
                }
            }else{
                break;
            }
        }
        if(res == -1){
            System.out.println("No");
        }
    }
    static int binarysearch(long[] arr, long tmp){
        int begin = 0;
        int end = arr.length - 1;
        int mid;
        while(begin <= end){
            mid = begin + (end - begin) / 2;
            if(arr[mid] == tmp){
                return mid;
            }else if(arr[mid] > tmp){
                end = mid - 1;
            }else{
                begin = mid + 1;
            }
        }
        return -1;
    }
}
  • 5
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

@u@

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值