二分法总结

出处:代码随想录 代码随想录 代码随想录 代码随想录

二分法的两种区间定义:

1.[left, right] 左闭右闭

2.[left, right) 左闭右开

一、target定义在左闭右闭区间内:[left, right]

此时:

  • while (left <= right) 要使用 <= ,因为left == right是有意义的,所以使用 <=
  • if (nums[middle] > target) right 要赋值为 middle - 1,因为当前这个nums[middle]一定不是target,那么接下来要查找的左区间结束下标位置就是 middle - 1
    public int search(int[] nums, int target) {
        // 避免当 target 小于nums[0] nums[nums.length - 1]时多次循环运算
        if (target < nums[0] || target > nums[nums.length - 1]) {
            return -1;
        }
        int left = 0, right = nums.length - 1;
        while (left <= right) {
            int mid = left + ((right - left) >> 1);
            if (nums[mid] == target)
                return mid;
            else if (nums[mid] < target)
                left = mid + 1;
            else {
                right = mid - 1;
            } 
        }
        return -1;
    }

 二、target定义在左闭右开区间内:[left, right)

  • while (left < right),这里使用 < ,因为left == right在区间[left, right)是没有意义的
  • if (nums[middle] > target) right 更新为 middle,因为当前nums[middle]不等于target,去左区间继续寻找,而寻找区间是左闭右开区间,所以right更新为middle,即:下一个查询区间不会去比较nums[middle]
    public int search(int[] nums, int target) {
        int left = 0, right = nums.length;
        while (left < right) {
            int mid = left + ((right - left) >> 1);
            if (nums[mid] == target)
                return mid;
            else if (nums[mid] < target)
                left = mid + 1;
            else {
                right = mid;
            }  
        }
        return -1;
    }

leetcode题目

    /**
     * 875.爱吃香蕉的珂珂
     * 珂珂喜欢吃香蕉。这里有 N 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,将在 H 小时后回来。

     珂珂可以决定她吃香蕉的速度 K (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 K 根。如果这堆香蕉少于 K 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉。  

     珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。

     返回她可以在 H 小时内吃掉所有香蕉的最小速度 K(K 为整数)。
     */

    /**
     * 为什么使用二分法查找呢?
     * 因为是查找是否存在一个最小速度x,如果它用x速度都能在h小时内吃完,那么比这个速度快肯定也能吃完
     *
     * 确定速度区间[1, 香蕉堆中最大的那堆香蕉的个数]
     *
     */
    public int minEatingSpeed(int[] piles, int h) {
        int low = 1;
        int high = Arrays.stream(piles).max().getAsInt();
        while (low < high) {
            // target定义在左闭右开区间,所以low==high是没有必要的
            int mid = low + (high - low) / 2;
            if (possible(piles, h, mid)) {
                // 因为while里的条件是low < high
                // 所以high更新的时候要【等于】mid
                // 因为是左闭右开区间,右即high==mid,但是却不会去搜索piles[mid]
                high = mid;
            } else {
                low = mid + 1;
            }
        }
        // 因为退出条件是 low和high相等,所以return low或者high都一样
        return high;
    }

    /**
     * 判断香蕉是否能吃完
     */
    private boolean possible(int[] piles, int h, int k) {
        int time = 0;
        for (int p : piles) {
            // 数组pile中的每一堆香蕉都用速度k吃,结果向上取整
            time += (p - 1) / k + 1; // 相当于ceiling方法
        }
        return time <= h;
    }


    /**
     * 1052. 爱生气的书店老板
     * 书店老板知道一个秘密技巧,能抑制自己的情绪,可以让自己连续 minutes 分钟不生气,但却只能使用一次。
     * 请你返回 这一天营业下来,最多有多少客户能够感到满意 。

     * 双指针法,
     */
    public int maxSatisfied(int[] customers, int[] grumpy, int minutes) {
        int total = 0;
        int n = customers.length;
        for (int i = 0; i < n; i++) {
            if (grumpy[i] == 0) {
                total += customers[i];
            }
        }
        int increase = 0;
        for (int i = 0; i < minutes; i++) {
            increase += customers[i] * grumpy[i];
        }
        int maxIncrease = increase;
        for (int i = minutes; i < n; i++) {
            increase = increase - customers[i - minutes] * grumpy[i - minutes] + customers[i] * grumpy[i];
            maxIncrease = Math.max(maxIncrease, increase);
        }
        return total + maxIncrease;
    }

用二分法找左右边界

左边界-leftBound也叫lowerBound

    public static int left_bound(int[] nums, int target) {
        int left = 0, right = nums.length - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] < target) {
                left = mid + 1;
            } else if (nums[mid] > target) {
                right = mid - 1;
            } else if (nums[mid] == target) {
                // 别返回,锁定左侧边界
                // 只搜索到就好的二分法这里就直接return mid了,但是寻找左边界时不直接返回
                // 继续让right = mid - 1,向左缩圈
                right = mid - 1;
            }
        }
        // 最后要检查 left 越界的情况
        if (left >= nums.length || nums[left] != target) {
            return -1;
        }
        return left;
    }

右边界-rightBound又叫upperBound

    public static int right_bound(int[] nums, int target) {
        int left = 0, right = nums.length - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] < target) {
                left = mid + 1;
            } else if (nums[mid] > target) {
                right = mid - 1;
            } else if (nums[mid] == target) {
                // 别返回,锁定右侧边界
                // 一般情况会找这个mid并返回,但是现在要找upperbound
                // 所以要向外扩出左边界
                left = mid + 1;
            }
        }
        // 最后要检查 right 越界的情况
        if (right < 0 || nums[right] != target) {
            return -1;
        }
        return right;
    }

一道习题

public class SnowMountainCost {
    /**
     * 某雪场共有 N 座雪山,数组 altitude中存储了各雪山海拔(精确到整数)。雪场出售新手票与老手票,新手区票价较高。
     * 若该雪场内最高海拔与最低海拔的差值大于 difference,则为老手区;否则为新手区。现在是滑雪活动旺季,雪场经营者希望获得更大收益,

     想要将整个雪场打造成新手雪场。改造某座雪山海拔高度的成本为:变更高度的平方。注意:
     * 变更高度仅可为整数;
     * 变更工程可增加雪山海拔,也可降低雪山海拔;
     * 请问雪场经营者改造需要投入的最少成本是多少(即:所有改造雪山的成本之和)?
     * 答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
     *
     * 示例 1:
     * 输入:altitude = [5,1,4,3,8], difference = 3
     * 输出:8
     * 解释:将 1 变成 3,8 变成 6 ,这时最高是 6,最小是 3,相差不超过 3。需要成本为:2^2 + 2^2 = 8
     * 示例 2:
     * 输入:altitude = [1,2,99999,3,100000], difference = 3
     * 输出:998679962
     * 解释:将 1,2 和 3 分别变为 40000,将 99999 和 100000 分别变为 40003,此时最高为 40003,最低为 40000,相差不超过 3,
     * 此时需要成本为 11998680039,为最小值,取模后为 998679962
     * 提示:
     *
     * 1 <= altitude.length <= 10^5
     * 1 <= altitude[i],difference <= 10^5
     */
    public static int minPaymentBinarySearchMinMax(int[] altitude, int difference) {
        /*
        x表示雪场内最后的最低山峰,f(x)表示最后的成本
         */
        Arrays.sort(altitude);
        // left应为最低altitude中的最低海拔
        // right应为最高海拔
        // 二分法寻找一个在最低海拔和最高海拔之间的,
        // 且和最高海拔的差值要控制在difference内,让所有雪场都是新手区
        /**
         * 题目解法就是找到一个区间[h, h + difference]
         * 对于超过这个区间的山峰进行改造,改造值的平方和就是h下的成本f(h)
         * 首先,h的取值在min和max - difference的范围内,用二分法查找取值范围
         *
         */
        int left = Arrays.stream(altitude).min().getAsInt();
        System.out.println(" ===== left:" + left);
        int right = Arrays.stream(altitude).max().getAsInt();
        System.out.println(" ===== right:" + right);
        long cost = Long.MAX_VALUE;
        BigInteger minCost = BigInteger.valueOf(0L);
        while (left <= right) {
            int mid = left + (right - left) / 2;
            BigInteger leftCost = calculateCost(mid - 1, difference, altitude);
            BigInteger midCost = calculateCost(mid, difference, altitude);
            BigInteger rightCost = calculateCost(mid + 1, difference, altitude);
            if (midCost.compareTo(leftCost) < 0 && midCost.compareTo(rightCost) < 0) {
                minCost = midCost;
                break;
            } else if (leftCost.compareTo(rightCost) > 0) {
                // 这里的调整是因为f(h)整体是个凹函数
                // 如果leftcost > rightcost,则整体需要向right移动
                left = mid + 1;
                minCost = leftCost;
            } else {
                // 理由同上
                right = mid - 1;
                minCost = rightCost;
            }
        }

        cost = Math.min(cost, minCost.divideAndRemainder(BigInteger.valueOf(1000000007L))[1].longValue());
        return (int)cost;
    }

    private static BigInteger calculateCost(int height, int difference, int[] altitude) {
        // 因为成本可能非常大,所以需要大数来控制
        BigInteger oneCost = BigInteger.valueOf(0L);
        for (int a : altitude) {
            if (a < height) {
                oneCost = oneCost.add(BigInteger.valueOf(height - a)
                        .multiply(BigInteger.valueOf(height - a)));
            } else if (a > height + difference) {
                oneCost = oneCost.add(BigInteger.valueOf(a - height - difference)
                        .multiply(BigInteger.valueOf(a - height - difference)));
            }
        }
        return oneCost;
    }

    public static void main(String[] args) {
        int[] altitude = new int[]{5, 1, 4, 3, 8};
        int difference = 3;
        System.out.println(minPaymentBinarySearchMinMax(altitude, difference));
    }
}

再一道习题:

    /**
     * 道路是直线,多名快递员在道路上完成取件任务,(多名快递员自由行动,互不影响)。
     * 快递员初始位置记录于数组persons,取件位置记录于数组tasks中(值不重复)。
     * 每个快递员单位时间可向左或向右走一个单位长度,或停留不动。
     * 当快递员到达取件位置即完成任务(取件花费时间忽略),该快递员可以继续参与取件。
     * 快递员并行作业,请返回最少需要多少单位时间才能完成所有取件任务。
     *
     * 示例:
     * 输入:persons= [2,8,7] tasks=[1,3,11,7] 输出3
     * 解释:三个快递员并行作业,轨迹依次为:
     * 2 ——1——2——3
     * 8——9——10——11
     * 7 不用动
     * 所以最少需要三个单位时间(并行最大时间,不是求和)
     */

    /**
     * 题目求的是完成所有取件任务的最小值,也就是是说存在一个时间的最小值,当这个值为x,能完成就是f(x) = true
     * 小于x时不能完成,就是f(x) = false,所以可以用二分法搜索这个值
     *
     * 还有一个策略,考虑最左边的快递员,假设它左边还有任务i,如果向左走,看能否完成最左的任务,
     * 如果到不了最左的任务说明x太小了,f(x)直接返回false
     * 如果能完成最左边的任务,就考虑两种走法:
     *                                   1.先左后右走满x
     *                                   2.先右后左走满x,找到两种走法走到右边最远的距离r,在[task[i], r]范围内的任务都完成
     *                                   3.如果此时最左的未完成任务是在这个快递员右边,则一路向右走满x,
     *                                   [person[j], person[j] + x]范围内的任务都已完成
     */
    public int expressDelivery(int[] tasks, int[] persons) {
        Arrays.sort(tasks);
        Arrays.sort(persons);
        int left = 0;
        int right = tasks[tasks.length - 1];
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (check(tasks, persons, mid)) {
                right = mid;
            } else {
                left = mid + 1;
            }
        }
        return left;
    }

    private boolean check(int[] tasks, int[] persons, int x) {
        int maxComplete = 0;
        int maxReach;
        for (int i = 0; i < persons.length; i++) {
            if (persons[i] - tasks[maxComplete] > x) {
                return false;
            }
            if (persons[i] - tasks[maxComplete] > 0) {
                maxReach = Math.max(tasks[maxComplete] + (x - persons[i] + tasks[maxComplete]),
                        (x + tasks[maxComplete] + persons[i]) / 2);
            } else {
                maxReach = persons[i] + x;
            }
            while (tasks[maxComplete] <= maxReach) {
                if (maxComplete < tasks.length) {
                    maxComplete++;
                }
            }
        }
        if (maxComplete == tasks.length) {
            return true;
        } else {
            return false;
        }
    }

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值