二分查找的典型问题(三):判别条件复杂的二分查找问题

目标变量和另一个变量有相关关系(一般而言是线性关系),目标变量的性质不好推测,但是另一个变量的性质相对容易推测。这样的问题的判别函数通常会写成一个函数的形式。

例题一

在这里插入图片描述
分析:题目中说,最小速度 K 是一个整数,并且我们知道这个 K 不可能无限大,珂珂一个小时能吃的香蕉最多为一整堆香蕉的个数。因此 K 是一个有范围的整数。如果我们做多了二分查找的问题,就知道这样的问题可以考虑使用二分查找去做,事实上,这道问题的解题思路就是我们在上一节介绍过的「二分答案」。

根据题意,我们不难分析出,如果珂珂每个小时吃的香蕉数越少,那么她吃完所有香蕉的耗时就越多;相应地,如果珂珂每个小时吃的香蕉数越多,那么她吃完所有香蕉的耗时就越少。

题目要我们找的最小速度,而限制是 H (吃完香蕉的所有时间)。因此我们就可以根据「吃完香蕉的所有时间」的限制,通过二分搜索的办法,得到最小的速度。

查找的是最小速度。由于题目限制了珂珂一个小时之内只能选择一堆香蕉吃,因此速度最大值就是这几堆香蕉中,数量最多的那一堆。速度的最小值是 11,还是因为珂珂一个小时之内只能选择一堆香蕉吃,因此:「每堆香蕉吃完的耗时 = 这堆香蕉的数量 / 珂珂一小时吃香蕉的数量」。根据题意,这里的 / 在不能整除的时候,需要上取整

先根据珂珂每小时吃掉的香蕉数,计算吃完所有香蕉的耗时,再根据下面的逻辑逼近到最小速度 a :

  • 如果耗时大于 H ,则说明速度太慢了,下一轮搜索的时候需要从 a + 1 开始;

如果耗时小于等于 H ,则说明速度或者刚好合适,或者太慢了,下一轮搜索的时候速度至多是 a (这里不能把速度 a 的值排除掉)

public class Solution {

    public int minEatingSpeed(int[] piles, int H) {
        int maxVal = 1;
        for (int pile : piles) {
            maxVal = Math.max(maxVal, pile);
        }

        // 速度最小的时候,耗时最长
        int left = 1;
        // 速度最大的时候,耗时最短
        int right = maxVal;

        while (left < right) {
            int mid = left + (right - left) / 2;

            if (calculateSum(piles, mid) > H) {
                // 耗时太多,说明速度太慢了,下一轮搜索区间在 [mid + 1, right]
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        return left;
    }

    /**
     * 如果返回的小时数严格大于 H,就不符合题意
     *
     * @param piles
     * @param speed
     * @return 需要的小时数
     */
    private int calculateSum(int[] piles, int speed) {
        int sum = 0;
        for (int pile : piles) {
            // 上取整可以这样写
            sum += (pile + speed - 1) / speed;
        }
        return sum;
    }
}

复杂度分析:

  • 时间复杂度:O(N log max(piles))O(Nlogmax(piles)),这里 N 表示数组 piles 的长度。我们在 [1, max{piles}] 里使用二分查找定位最小速度,而每一次执行判别函数的时间复杂度是 O(N);
  • 空间复杂度:O(1),算法只使用了常数个临时变量。

例题二

在这里插入图片描述
分析:

  • 数组中的元素全部非负,可能有 0;
  • 分成 m 个非空的连续子数组,这一句话中非空连续是很重要的,如果没有连续我们设计的算法就会有所改变;
  • 各自和的最大值最小,这一句话的意思是:对分割的每一个数组求和的最大值,根据题目的示例,让我们输出的是,这个最大值在某一种最优分割下,最小值是多少。这种分割不需要我们描述出来,但是题目要求我们输出这种最优分割的各个组里,组内所有元素的和的最大值。

题目要我们找在最优分割下,各个组的和的最大值,在最小情况下是多少,这个值是一个整数,极端情况下,所有元素被分在一组,因此,这个整数的最大值是数组里所有元素的和,而最小值就是数组里所有元素的最小值。(请大家体会,这里题目中说元素都是非负数的原因。)这样的场景就是求一个有范围的整数,可以考虑使用「二分答案」。

由于子数组必须是原始数组的连续子数组,并且所有元素非负,那么一个元素一定会属于一个区间,我们可以这样设计算法,假设当前分在同一个组里的所有的元素的和为 a。分割的策略是:从左到右,做加和,只要是加和 小于等于 a 就分为一组,一旦严格大于 a ,则另起一组。于是有下面的 单调性

  • 如果 a 的值很小,分割的组数就会很多;
  • 如果 a 的值很大,分割的组数就会很少。
    即:各个组内元素的和的最大值 a 与分割的组数 M (为了与题目中的 m 区分,这里用 M) 是单调递减的关系;利用这个单调递减的关系,使用二分查找算法,调整 a 的值,把 M 逼近到 m 。
public class Solution {

    public int splitArray(int[] nums, int m) {
        int max = 0;
        int sum = 0;

        // 计算「子数组各自的和的最大值」的上下界
        for (int num : nums) {
            max = Math.max(max, num);
            sum += num;
        }

        // 使用「二分查找」确定一个恰当的「子数组各自的和的最大值」,使得它对应的「子数组的分割数」恰好等于 m
        int left = max;
        int right = sum;
        while (left < right) {
            int mid = left + (right - left) / 2;

            int splits = split(nums, mid);
            if (splits > m) {
                // 如果分割数太多,说明「子数组各自的和的最大值」太小,此时需要将「子数组各自的和的最大值」调大
                // 下一轮搜索的区间是 [mid + 1, right]
                left = mid + 1;
            } else {
                // 下一轮搜索的区间是上一轮的反面区间 [left, mid]
                right = mid;
            }
        }
        return left;
    }

    /***
     *
     * @param nums 原始数组
     * @param maxIntervalSum 子数组各自的和的最大值
     * @return 满足不超过「子数组各自的和的最大值」的分割数
     */
    private int split(int[] nums, int maxIntervalSum) {
        // 至少是一个分割
        int splits = 1;
        // 当前区间的和
        int curIntervalSum = 0;
        for (int num : nums) {
            // 尝试加上当前遍历的这个数,如果加上去超过了「子数组各自的和的最大值」,就不加这个数,另起炉灶
            if (curIntervalSum + num > maxIntervalSum) {
                curIntervalSum = 0;
                splits++;
            }
            curIntervalSum += num;
        }
        return splits;
    }
}

复杂度分析:

  • 时间复杂度:O(Nlog∑nums),这里 N 表示输入数组的长度,∑nums 表示输入数组的和,代码在[max(nums),∑nums] 区间里使用二分查找找到目标元素,而每一次判断分支需要遍历一遍数组,时间复杂度为 O(N);
  • 空间复杂度:O(1) ,只使用到常数个临时变量。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值