二分搜索应用 II

1. 题目列表

题目列表:

序号 题目 难度
1 410. 分割数组的最大值 困难
2 658. 找到 K 个最接近的元素 中等
3 2594. 修车的最少时间 中等
4 2560. 打家劫舍 IV 中等
5 162. 寻找峰值 中等
6 1552. 两球之间的磁力 中等

2. 应用

2.1. Leetcode 410. 分割数组的最大值

2.1.1. 题目

410. 分割数组的最大值

给定一个非负整数数组 \(nums\) 和一个整数 \(k\) ,你需要将这个数组分成 \(k\) 个非空的连续子数组。设计一个算法使得这 \(k\) 个子数组各自和的最大值最小。

示例 1:

输入:\(nums = [7,2,5,10,8]\), \(k = 2\)
输出:\(18\)
解释:
一共有四种方法将 nums 分割为 2 个子数组。其中最好的方式是将其分为 \([7,2,5]\)\([10,8]\) 。因为此时这两个子数组各自的和的最大值为 \(18\),在所有情况中最小。

提示:

\(1 <= nums.length <= 1000\)
\(0 <= nums[i] <= 10^6\)
\(1 <= k <= min(50, nums.length)\)

2.1.2. 解题思路

题目中要求将数组 \(nums\) 分成 \(k\) 个非空连续子数组,使得每个子数组的和尽可能小,同时,分割的子数组个数又恰好等于 \(k\),那么,对于分割的方式必然存在单调性,因此,我们可以使用二分搜索的方式求解。

设数组 \(nums\) 的长度为 \(n\),显然,每个区间和的最小值就是数组中的最大值,同时,不超过数组所有元素的和,那么

  • 二分查找的右侧区间就是 \(left = \max_{i=0}^{n}nums[i]\)

  • 二分查找的右侧区间就是 \(right = \sum_{i=0}^{n}nums[i]\)

那么,只需要在闭区间 \([left, right] = [\max_{i=0}^{n}nums[i], \ \sum_{i=0}^{n}nums[i]]\) 内,多次进行二分查找,只要某一个数组和恰好可以使数组分割为 \(k\),即可退出查找。

2.1.3. 代码实现

class Solution {
    public int splitArray(int[] nums, int k) {
        int sum = 0;
        int maxNum = 0;
        for (int num : nums) {
            sum += num;
            maxNum = Math.max(maxNum, num);
        }

        int left = maxNum;
        int right = sum;
        while (left < right) {
            int mid = left + (right - left) / 2;
            // 分割次数大于k,说明每一段的目标和太小了,需要缩小左区间
            if (check(nums, k, mid)) {
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        return right;
    }

    private boolean check(int[] nums, int k, int target) {
        int splitCount = 1; // 以当前的区间中点作为子数组的和,可以分割的次数
        int sum = 0;
        for (int num : nums) {
            if (sum + num <= target) {
                sum += num;
            } else {
                // 若分割次数大于k,则需要缩小区间左边界
                splitCount++;
                if (splitCount > k) {
                    return true;
                }
                sum = num;
            }
        }
        return false;
    }
}

2.2. Leetcode 658. 找到 K 个最接近的元素

2.2.1. 题目

658. 找到 K 个最接近的元素

给定一个 排序好 的数组 arr ,两个整数 k 和 x ,从数组中找到最靠近 x(两数之差最小)的 k 个数。返回的结果必须要是按升序排好的。
整数 a 比整数 b 更接近 x 需要满足:

  • \(|a - x| < |b - x|\) 或者
  • \(|a - x| == |b - x|\)\(a < b\)

示例 1:

输入:arr = [1,2,3,4,5], k = 4, x = 3
输出:[1,2,3,4]

2.2.2. 解题思路

假设数组 \(nums\) 的长度为 \(n\),由于原始的数组 \(nums\) 是有序的,因此,我们可以利用二分查找的思路,找到一个满足条件的,且长度为 \(k\) 的数组子区间:

\[[left, right], right - left = k \]

跟查找一个具体的数值不同的是,这里我们需要查找一个符合条件的子区间,并且在查找过程中,因为我们希望找到与 \(x\) 差值更小并且数组的索引更小,所以,我们只需要找到这个子区间的左侧端点即可,这样,题目就等价于通过二分查找求左边界的问题了,因此,我们可以将初始查找区间设置为:\([0, n - 1 - k]\)

这样,每次比较的时候,需要比较下一个区间的起点 \(nums[mid]\) 和区间终点 \(nums[mid + k]\)\(x\) 的差值,即可缩小差值范围了。

2.2.3. 复杂度

  • 时间复杂度: \(O(logn + k)\)

  • 空间复杂度: \(O(n)\)

2.2.4. 代码实现

class Solution {
    public List<Integer> findClosestElements(int[] arr, int k, int x) {
        int left = 0, right = arr.length - k - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (x - arr[mid] <= arr[mid + k] - x) {
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }

        List<Integer> result = new ArrayList<>();
        for (int i = left; i < left + k; i++) {
            result.add(arr[i]);
        }
        return result;
    }
}

2.3. Leetcode 2594. 修车的最少时间

2.3.1. 题目

2594. 修车的最少时间

给你一个整数数组 ranks ,表示一些机械工的 能力值 。\(ranks_i\) 是第 i 位机械工的能力值。能力值为 r 的机械工可以在 \(r \times n^2\) 分钟内修好 n 辆车。
同时给你一个整数 cars ,表示总共需要修理的汽车数目。
请你返回修理所有汽车 最少 需要多少时间。
注意:所有机械工可以同时修理汽车。

示例 1:

输入:ranks = [4,2,3,1], cars = 10
输出:16
解释:

  • 第一位机械工修 2 辆车,需要 4 * 2 * 2 = 16 分钟。
  • 第二位机械工修 2 辆车,需要 2 * 2 * 2 = 8 分钟。
  • 第三位机械工修 2 辆车,需要 3 * 2 * 2 = 12 分钟。
  • 第四位机械工修 4 辆车,需要 1 * 4 * 4 = 16 分钟。
    16 分钟是修理完所有车需要的最少时间。

2.3.2. 解题思路

对于能力值为 \(r\) 的工人可以在 \(r \times n^2\) 分钟内修好 \(n\) 辆车,那么,他在 \(t\) 分钟可以修好的车量数为:

\[n = \sqrt{\frac{t}{r}} \]

对于修车的时间 \(t\) 必然存在单调性,即

  • 如果 \(t\) 分钟内,所有的汽车都可以修理完,那么,大于等于 \(t\) 分钟时,也可以将所有汽车修理完;

  • 如果 \(t\) 分钟内,所有的汽车都不能修理完,那么,小于等于 \(t\) 分钟时,也不能将所有汽车修理完。

那么,我们可以枚举时刻来查找满足条件的时间。

二分查找的边界:

  • 下界:由于车辆最少有 \(1\) 辆,所以,修改好一辆车至少需要一分钟,因此,下边界为 \(1\)

  • 上界:任何一个人独立完成工作的所需的时间,必然大于所有人一起完成工作所需的时间,因此,可以取任意一个人独立完成所需的时间为上边界。

所以,二分查找的区间范围:

\[[left, right] = [1, rank[i] \times cars ^ 2], 0 \le i \le k - 1 \]

其中,\(k\) 为数组 \(ranks\) 的长度。

因此,我们可以在闭区间内使用二分查找,找到满足条件的左边界即可。

2.3.3. 复杂度

  • 时间复杂度: \(O(nlog(rank[0] \times cars ^ 2))\)

    其中,\(n\) 是数组 \(ranks\) 的长度。

  • 空间复杂度: \(O(1)\)

2.3.4. 代码实现

class Solution {
    public long repairCars(int[] ranks, int cars) {
        // 任何一个工人修完所有车辆的时间一定大于等于最优解
        long left = 1, right = (long) ranks[0] * cars * cars;
        while (left < right) {
            long mid = left + (right - left) / 2;
            if (check(ranks, cars, mid)) {
                right = mid;
            } else {
                left = mid + 1;
            }
        }
        return left;
    }

    private boolean check(int[] ranks, int cars, long minute) {
        long count = 0;
        for (int rank : ranks) {
            count += (long) Math.sqrt(1.0 * minute / rank);
        }
        return count >= cars;
    }
}

2.4. Leetcode 2560. 打家劫舍 IV

2.4.1. 题目

2560. 打家劫舍 IV

沿街有一排连续的房屋。每间房屋内都藏有一定的现金。现在有一位小偷计划从这些房屋中窃取现金。
由于相邻的房屋装有相互连通的防盗系统,所以小偷 不会窃取相邻的房屋
小偷的 窃取能力 定义为他在窃取过程中能从单间房屋中窃取的 最大金额 。
给你一个整数数组 nums 表示每间房屋存放的现金金额。形式上,从左起第 i 间房屋中放有 nums[i] 美元。
另给你一个整数 k ,表示窃贼将会窃取的 最少 房屋数。小偷总能窃取至少 k 间房屋。
返回小偷的 最小 窃取能力。

示例 1:

输入:nums = [2,3,5,9], k = 2
输出:5
解释:
小偷窃取至少 2 间房屋,共有 3 种方式:

  • 窃取下标 0 和 2 处的房屋,窃取能力为 max(nums[0], nums[2]) = 5 。
  • 窃取下标 0 和 3 处的房屋,窃取能力为 max(nums[0], nums[3]) = 9 。
  • 窃取下标 1 和 3 处的房屋,窃取能力为 max(nums[1], nums[3]) = 9 。
    因此,返回 min(5, 9, 9) = 5 。

2.4.2. 解题思路

通过分析,容易看出,小偷的偷窃能力 \(capability\) 范围是:

\[capability \in [\min^{n}_{i=0}(nums[i]), \max^{n}_{i=0}(nums[i])] \]

对于每一个能力值 \(capability\) ,它显然满足单调性,即:

  • 大于能力值的房间都不能偷窃;

  • 小于能力值的房间,且其相邻房间未被偷窃,那么,它可以被偷窃。

因此,我们可以考虑使用二分查找,满足条件的最小能力值。

注意,在查找过程中,我们需要用到贪心的思想,即对于每一个能力值,越早偷窃,留给剩余房子偷窃的机会就越多。

如果房间数量为 \(n\),那么,能偷窃房间不超过 \(\frac{n}{2}\),因此,在偷窃过程中,遇到能偷窃的房间,需要尽可能地多偷窃更多的房间。

2.4.3. 复杂度

  • 时间复杂度: \(O(nlog(\max^{n}_{i=0}(nums[i]) - \min^{n}_{i=0}(nums[i]))\)

  • 空间复杂度: \(O(1)\)

2.4.4. 代码实现

class Solution {
    public int minCapability(int[] nums, int k) {
        int left = Integer.MAX_VALUE, right = Integer.MIN_VALUE;
        for (int num : nums) {
            right = Math.max(right, num);
            left = Math.min(left, num);
        }

        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (check(nums, k, mid)) {
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }

        return left;
    }

    private boolean check(int[] nums, int k, int capability) {
        int count = 0;
        boolean visited = false;
        for (int num : nums) {
            if (num <= capability && !visited) {
                count++;
                visited = true;
            } else {
                visited = false;
            }
        }
        return count >= k;
    }
}

2.5. Leetcode 162. 寻找峰值

2.5.1. 题目

162. 寻找峰值

峰值元素是指其值严格大于左右相邻值的元素。
给你一个整数数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。
你可以假设 \(nums[-1] = nums[n] = -\infty\)
你必须实现时间复杂度为 \(O(log n)\) 的算法来解决此问题。

示例 1:

输入:nums = [1,2,1,3,5,6,4]
输出:1 或 5
解释:

  • 你的函数可以返回索引 1,其峰值元素为 2;
  • 或者返回索引 5, 其峰值元素为 6。

2.5.2. 解题思路

从题意可知,峰值元素一定严格大于左右相邻值的元素。

题目中可能有多个峰值元素,那么,对于任意两个相邻的峰谷之间的局部元素它是满足单调性的,并且数组中没有相邻的重复元素,因此,我们可以通过二分查找,确定峰值的位置。

每次二分时区间的中点,必然落在某两个相邻的峰谷之间,即

  • 如果元素出现在峰值的左侧,移动左指针;

  • 如果元素出现在峰值的右侧,移动右指针;

这样,就可以通过二分查找,在 \(O(logn)\) 的复杂度内完成查找。

这里,我们可以将题目等价于一个在闭区间内查找左边界的二分查找,第一个不满足条件的元素,就是题目的答案之一。

类似地,我们也可以将其看成查找右边界的二分查找。

2.5.3. 复杂度

  • 时间复杂度: \(O(log(n))\)

  • 空间复杂度: \(O(1)\)

2.5.4. 代码实现

  • 查找左边界的思路
class Solution {
    public int findPeakElement(int[] nums) {
        int n = nums.length;
        if (n == 1) {
            return 0;
        }
        int left = 0, right = n - 1;
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] < nums[mid + 1]) {
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        return left;
    }
}

2.6. Leetcode 1552. 两球之间的磁力

2.6.1. 题目

1552. 两球之间的磁力

2.6.2. 解题思路

假设数组 \(postion\) 长度为 \(n\),我们考虑先对数组 \(postion\) 排序。

那么,题目可以等价于:从有序数组 \(postion\) 中任意选择 \(m\) 个元素,对于这 \(m\) 个元素,我们对选出的相邻元素进行两两求差值可以得到 \(m - 1\) 个差值:

\[f(i,j) = position[j] - position[i], 1 \le i \lt j \le n \]

求所有组合中,最小的 \(f(i, j)\) 的最大值。

这是一个求解极小值极大化的题目,显然,为了使最小差值最大,我们每次需要挑选的两个数字 \((position[i], position[j])\) 的间距都要尽可能远

假设所求的目标值为 \(target\),显然,这样的组合数求解具有二段性,即:

  • \(f_{min}(i,j) \gt target\) 时,能选择的数字个数必然小于 \(m\)

  • \(f_{min}(i,j) \le target\) 时,能选择的数字个数必然大于等于 \(m\)

那么,我们使用二分查找求解满足条件的 \(f(i, j)\)

2.6.3. 复杂度

  • 时间复杂度: \(O(nlogn + nlog(\max^n_{i=0}(postion[i])))\)

    其中,排序需要的时间复杂度为 \(O(logn)\),二分查找的复杂度为 \(nlog(\max^n_{i=0}(postion[i]))\)

  • 空间复杂度: \(O(logn)\)

    排序需要的栈空间,复杂度为 \(O(logn)\)

2.6.4. 代码实现

class Solution {
    public int maxDistance(int[] position, int m) {
        Arrays.sort(position);
        int n = position.length;
        int result = -1;
        int left = 0, right = position[n - 1] - position[0];
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (check(position, mid, m)) {
                left = mid + 1;
                result = mid;
            } else {
                right = mid - 1;
            }
        }
        return result;
    }

    private boolean check(int[] position, int distance, int m) {
        int last = position[0], count = 1;
        for (int i = 1; i < position.length; i++) {
            if (position[i] - last >= distance) {
                last = position[i];
                count += 1;
            }
        }
        return count >= m;
    }
}

3. 总结

二分查找不需要关注区间内的元素具有什么性质,而是需要关注区间外面的元素具有什么性质。


参考:

posted @ 2024-01-21 20:53  LARRY1024  阅读(4)  评论(0编辑  收藏  举报