🥂前言:

🌴🌴二分搜索虽然属于比较简单的算法,但是代码上的细节处理不当的话会产生一些难以发现的bug,比如执行超时,返回错误值~🤷‍♀️

⭐⭐加上在蓝桥杯的程序题中不会直接考二分查找这么简单的算法。想要解决二分搜索有关的问题,除了要熟悉二分搜索的套路,还要针对实际问题对二分搜索的基本框架做出修改,这就对参加蓝桥杯杯的同学提出了更高的要求。

🌻🌻但是不要慌,这里送给大家一个便于记忆,易于修改,又不会出错的二分搜索模板,在解决蓝桥杯的二分搜索问题时可以直接套用模板轻松秒杀。😎

题目传送门: 🚀🚀🚀

题目链接
LeetCode-704.二分查找https://leetcode-cn/problems/binary-search/
蓝桥杯省赛-分巧克力https://www.lanqiao/problems/99/learning/

🍬如何二分?

👉🏻二分搜索也可以叫作折半搜索(查找),在一个有序的数组中查找给定元素位置的一个算法。如果给定的是一个有序的数组,采用二分法可以在 O ( l o g N ) O(log{N}) O(logN)的时间复杂度下完成搜索任务。

👩🏻‍🏫这里以LeetCode上的第704题为例,介绍二分搜索的过程。

数组已经是排好顺序的,我们就可以利用这一特点,减少搜索时间。

采用二分的方式,我们需要三个指针left,right与mid,分别指向数组的开始,末尾,与中间位置,其中mid是left与right的平均值,即mid始终指向数组最中间的位置。

初始时 l e f t = 0 left=0 left=0 r i g h t = 5 right=5 right=5 m i d = ( l e f t + r i g h t ) ÷ 2 = 2 mid=(left+right)÷2 =2 mid=(left+right)÷2=2,mid指向的值为3,也就是数组中间的值是3,我们可以发现由于数组是递增排序的,中间位置的左侧都会比3小,右侧都会比3大,并且目标值9是大于3的,因此我们可以忽略左半部分,直接在右半部分查找。

接下来就可以重复上面的方法,确定目标值是在mid所指向位置的左边还是右边,不断的对数组二分,直到找到目标值所在的位置。

👩🏻‍🏫✨✨✨

明白了二分搜索的过程,接下来给出算法的模板,这里的二分搜索与常见算法书上的代码略有区别,为了方便记忆和增加代码的灵活性,对二分搜索的框架做了一定修改。

🥇参考代码(Python):

class Solution:
    def search(self, nums: List[int], target: int) -> int:
        left, right = 0, len(nums) - 1
        while left + 1 < right:
            mid = int((left + right)/2)
            if nums[mid] == target:
                return mid
            elif nums[mid] > target:
                right = mid
            else:
                left = mid
        if nums[left] == target:
            return left
        if nums[right] == target:
            return right
        return -1

🏅代码巧妙的地方:

  • 一些算法教程的循环终止条件正常会写成while(left <= right),而这里写成while(left+1 < right),这样做的好处是后面修改二分区间时left与right的值可以直接写成left=midright=mid,不用考虑是否要加一或是减一,便于记忆。

  • 由于修改了循环终止条件,最终left与right会分别指向两个临近的位置,然后退出循环,此时需要在循环结束以后需要分别判断这两个位置是不是要找的目标值(target),以防出现下面的情况:

    比如要查找的值是2或者是5,进过若干次循环后,left与right指向两个临近的位置,left + 1 < right不成立,退出循环,而此时left与right指向的值还没有判断是否与我们要查找的目标值相等,所以在循环结束以后,还要单独对left与right指向的位置判断一次。


🍑实战演练:

有了这么牛🍺的二分模板,在蓝桥杯的题目中到底怎么使用呢?别急,接下来就通过一道蓝桥杯省赛真题,带大家见识一下此模板的强大之处。

🎈蓝桥真题-分巧克力:

题目描述
儿童节那天有 K 位小朋友到小明家做客。小明拿出了珍藏的巧克力招待小朋友们。
小明一共有 N 块巧克力,其中第 i 块是 H i ∗ W i H_i*W_i HiWi 的方格组成的长方形。为了公平起见,
小明需要从这 N 块巧克力中切出 K 块巧克力分给小朋友们。切出的巧克力需要满足:

  1. 形状是正方形,边长是整数;
  2. 大小相同;

例如一块 6x5 的巧克力可以切出 6 块 2x2 的巧克力或者 2 块 3x3 的巧克力。
当然小朋友们都希望得到的巧克力尽可能大,你能帮小明计算出最大的边长是多少么?

输入描述
第一行包含两个整数 N,K(1<= N,K<= 1 0 5 10^5 105)。

以下 N 行每行包含两个整数 H i , W i H_i,W_i Hi,Wi (1<= H i H_i Hi W i W_i Wi<= 1 0 5 10^5 105)。

输入保证每位小朋友至少能获得一块 1x1 的巧克力。

输出描述
输出切出的正方形巧克力最大可能的边长。

输入输出样例
示例

输入

2 10
6 5
5 6

输出

2

运行限制

  • 最大运行时间:2s
  • 最大运行内存: 256M

🍋解题思路:

👀对于长度为a,宽度为b的巧克力,若切成边长为side的正方形,最多可以分成的块数: ( a ÷ s i d e ) × ( b ÷ s i d e ) (a \div{side}) \times (b \div{side}) a÷side×b÷side

📍如果边长为 n × n n \times n n×n的巧克力能满足条件,那么边长小于n的情况也一定能满足条件。因此我们可以从分成边长为1x1的巧克力依次向后枚举,判断分成的块数是否满足条件(总块数大于或等于小朋友数),直到找到满足此条件的最大值。

📍但是题目中巧克力的最大边长和小朋友人数的数据规模最大为 1 0 5 10^5 105,时间复杂度为 O ( 1 0 5 × 1 0 5 ) O(10^5 \times{10^5}) O(105×105),枚举会造成超时。由于我们在枚举可以分成的巧克力最大边长时,是从1向后枚举的,这里的枚举过程本身就是有序的,我们不妨将这个枚举过程用二分搜索来代替。

🍦AC代码(Java):

import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

public class Main {

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int chocolate = sc.nextInt();
        int kids = sc.nextInt();
        ArrayList<Integer> height = new ArrayList<>();
        ArrayList<Integer> weight = new ArrayList<>();
        for (int i = 0; i < chocolate; i++) {
            height.add(sc.nextInt());
            weight.add(sc.nextInt());
        }
        int res = binarySearch(kids, height, weight);
        System.out.println(res);
    }

//    二分搜索,找到满足条件的边长最大值。
    public static int binarySearch(int kids, List<Integer> height, List<Integer> weight) {
        int left = 1, right = 10000;
        while (left + 1 < right) {
            int mid = left + ((right - left) >> 1);
//            由于要找最大值,所以不要直接返回mid,继续向后搜索。
            if (isValid(kids, mid, height, weight))
                left = mid;
            else
                right = mid;
        }
//        循环结束时,left与right指向的位置还没判断,其中有一个就是我们要找的答案。
//        这里先判断right,right指向的值满足条件,left也满足,反之却不然。
        if (isValid(kids, right, height, weight))
            return right;
        else
            return left;
    }

//    判断分成边长为sideOfChocolate的巧克力块数是否大于小朋友数。
    public static boolean isValid(int kids, int sideOfChocolate, List<Integer> height, List<Integer> weight) {
        int count = 0;
        for (int i = 0; i < height.size(); i++) {
            count += (height.get(i) / sideOfChocolate) * (weight.get(i) / sideOfChocolate);
        }
        return count >= kids;
    }
}

🍹需要注意的几点:

  • 选择二分搜索的对象时一定要选择排好序的,这里分成的巧克力边长从1开始枚举,因此将它改成二分,其实也就是对我们的最终答案二分。

  • 这里要找到满足条件的边长最大值,因此在循环中每次取得中间点时,如果满足条件先不要返回,而是将left=mid,继续搜索。

  • 循环的判断条件是left + 1 < right,像前面提到的一样,退出循环以后left与right的值还没判断,还要单独对left与right在判断一次,不要忘记。这也是这个二分搜索模板精妙的地方,循环结束后,left与right的值必然有一个就是我们要找的最大值。

  • 先判断left还是先判断right?

    这个就要因题而异了,比如在这道题里,left的值是小于right的,而边长大的巧克力满足条件,边长小的巧克力能切出更多块,也肯定满足条件,因此本题要先判断right。


🍌🍌🍌
二分搜索的题目千变万化,但是核心代码是不变的,要想将二分搜索运用自如,请熟读并背诵本文的二分模板,并在实战中多加运用,相信你很快就能掌握二分搜索这个算法技巧。😎
🍍🍍🍍
创作不易,如果觉得本文对你有所帮助的话请动动小手,给博主点个免费的赞吧。🙇🏻‍♀️
🍉🍉🍉
@作者:Mymel_晗,计科专业大学牲菜狗一枚,请大佬们多多关照~

更多推荐

【蓝桥杯Java组】送你一个不会出错的二分搜索模板