二分搜索应用 II

1. 题目列表

题目列表:

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]<=106
1<=k<=min(50,nums.length)

2.1.2. 解题思路

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

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

  • 二分查找的右侧区间就是 left=maxi=0nnums[i]

  • 二分查找的右侧区间就是 right=i=0nnums[i]

那么,只需要在闭区间 [left,right]=[maxi=0nnums[i], i=0nnums[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 需要满足:

  • |ax|<|bx| 或者
  • |ax|==|bx|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],rightleft=k

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

这样,每次比较的时候,需要比较下一个区间的起点 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 ,表示一些机械工的 能力值 。ranksi 是第 i 位机械工的能力值。能力值为 r 的机械工可以在 r×n2 分钟内修好 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×n2 分钟内修好 n 辆车,那么,他在 t 分钟可以修好的车量数为:

n=tr

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

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

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

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

二分查找的边界:

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

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

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

[left,right]=[1,rank[i]×cars2],0ik1

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

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

2.3.3. 复杂度

  • 时间复杂度: O(nlog(rank[0]×cars2))

    其中,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[mini=0n(nums[i]),maxi=0n(nums[i])]

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

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

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

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

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

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

2.4.3. 复杂度

  • 时间复杂度: O(nlog(maxi=0n(nums[i])mini=0n(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]=
你必须实现时间复杂度为 O(logn) 的算法来解决此问题。

示例 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 个元素,我们对选出的相邻元素进行两两求差值可以得到 m1 个差值:

f(i,j)=position[j]position[i],1i<jn

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

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

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

  • fmin(i,j)>target 时,能选择的数字个数必然小于 m

  • fmin(i,j)target 时,能选择的数字个数必然大于等于 m

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

2.6.3. 复杂度

  • 时间复杂度: O(nlogn+nlog(maxi=0n(postion[i])))

    其中,排序需要的时间复杂度为 O(logn),二分查找的复杂度为 nlog(maxi=0n(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. 总结

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


参考:

本文作者:LARRY1024

本文链接:https://www.cnblogs.com/larry1024/p/17978345

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   LARRY1024  阅读(7)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
💬
评论
📌
收藏
💗
关注
👍
推荐
🚀
回顶
收起
  1. 1 404 not found REOL
404 not found - REOL
00:00 / 00:00
An audio error has occurred.