二分查找解法思路分析
- 分割数组的最大值
给定一个非负整数数组 nums 和一个整数 k ,你需要将这个数组分成 k 个非空的连续子数组,使得这 k 个子数组各自和的最大值 最小。
返回分割后最小的和的最大值。
子数组 是数组中连续的部份。
示例 1:
输入:nums = [7,2,5,10,8], k = 2
输出:18
解释:
一共有四种方法将 nums 分割为 2 个子数组。
其中最好的方式是将其分为 [7,2,5] 和 [10,8] 。
因为此时这两个子数组各自的和的最大值为18,在所有情况中最小。
示例 2:
输入:nums = [1,2,3,4,5], k = 2
输出:9
示例 3:
输入:nums = [1,4,4], k = 3
输出:4
本题是一道非常经典的「最大值极小化」问题,题目中的关键字「非负整数」很关键,我们放在最后和大家强调。
最大值极小化的意思是:不同的参数,导致不同的最大值,求所有参数下这些最大值中的最小者。
「子数组各自和的最大值最小」这句话读起来有一点点绕,我们拆分一下:
输入数组是确定的,k 是确定的,其中一组的和多了,别的组的和就少;
对于特定的、分割成 k 组的分割,对每一组求和,选出它们的最大值 val;
我们需要找到所有的、分割成 k 组的分割中,val 的最小值。具体怎么拆,题目不用我们求,只需要我们计算出 val 的值。
观察到「数组各自和的最大值」和分割数,有如下关系:
- 如果设置「数组各自和的最大值」很大,会使得分割数很小;
- 如果设置「数组各自和的最大值」很小,会使得分割数很大。
这是呈现的 单调性,并且题目要我们找的是一个整数,因此可以使用二分查找。
编码细节
假设某个「数组各自和的最大值」mid 使得数组的分割数为 splits,根据 splits 与 k 的大小关系,可以分为如下 3 种情况:
情况 1(splits = k):当前 mid 有可能是答案,下一轮应该尝试更小的数值,因此设置 right = mid;
情况 2(splits > k):mid 太小导致 splits 太大,因此设置 left = mid + 1;
情况 3(splits < k):mid 太大导致 splits 太小,因此设置 right = mid - 1。
这里需要注意:
出现 right = mid,为了避免出现死循环,循环可以继续的条件需要写成 left < right,表示退出循环以后找到答案;
为了使得退出循环以后 left 与 right 重合(避免交叉越界带来的分类讨论),合并 right = mid 与 right = mid - 1 的情况。
下面我们确定查找的范围:
二分查找的下界是数组 nums 中的最大元素,这是因为数组的最大元素一定会属于某一组,否则无法分组。例如,对于数组 nums = [1, 2, 3, 4, 5],如果分割成若干子数组,其中一个子数组必然会包含 5,所以「子数组和的最大」值最小就是 5;
二分查找的上界是数组 nums 所有元素的和,这是因为如果把整个数组作为一个子数组(这是分割的一种极端情况,当 k = 1 时),那么子数组和的最大值就是数组所有元素的总和。例如,对于数组 nums = [1, 2, 3, 4, 5],其所有元素和为 1 + 2 + 3 + 4 + 5 = 15,这就是「子数组和最大值」可能达到的最大情况。
重点
最后我们来看「非负整数」这个前提为什么很重要。当数组元素为非负整数时,子数组的和具有单调性。如果我们增加子数组中元素的个数,那么子数组的和只会增加或者保持不变(当新增元素为 0 时),不会减少。在二分查找过程中,我们需要判断是否可以将数组分成 k 个非空连续子数组,使得每个子数组的和不超过某个给定的最大值。由于非负整数,保证了和具有单调性,我们可以从左到右遍历数组,依次累加元素,当累加和超过给定的最大值时,就开启一个新的子数组。如果数组元素可以为负数,那么在累加过程中,和可能会减少,这样就无法按照这种简单的方式进行子数组的划分,二分查找就无法正常工作
java
class Solution {
public int splitArray(int[] nums, int k) {
if(k>nums.length){
return 0;
}
/**
情况 1(splits = k):当前 mid 有可能是答案,下一轮应该尝试更小的数值,因此设置 right = mid;
情况 2(splits > k):mid 太小导致 splits 太大,因此设置 left = mid + 1;
情况 3(splits < k):mid 太大导致 splits 太小,因此设置 right = mid - 1。
如果设置「数组各自和的最大值」很大,会使得分割数很小;
如果设置「数组各自和的最大值」很小,会使得分割数很大。
*/
int sum=0;
int max=0;
for(int num:nums){
max=Math.max(num,max);
sum+=num;
}
if(k==nums.length){
return max;
}
int l=max;
int r=sum;
while(l<r){
int mid=(r-l)/2+l;
// 假如每个子数组的最大值为mid,那么有多少个子数组
// 个数越多,最大和越小!spilt越大,mid就越小;如果split超过了k,那么mid应该应该应该增大
int splits=split(nums,mid);
// 这里和二分不一样!以前是nums[mid]越大,超过了k,那么mid就该越小,今天的是,nums【mid】越大,一旦超过了k,mid就应该越大
// 相当于找到第一个大于xxx的,用二分
if(splits>k){
l=mid+1;
}else{
r=mid;
}
}
// l和r表示的是值
// 要找最小的,在坐标轴上
return l;
}
/**
* @param nums 原始数组
* @param maxIntervalSum 子数组各自的和的最大值
* @return 满足不超过「子数组各自的和的最大值」的分割数
*/
public int split(int nums[],int max_value){
int splits=1;
// 注意,不超过!!!
int currentSum=0;
for(int num:nums){
if(currentSum+num>max_value){
splits++;
currentSum=0;
}
currentSum+=num;
}
return splits;
}
}
1482. 制作 m 束花所需的最少天数
提示
给你一个整数数组 bloomDay,以及两个整数 m 和 k 。
现需要制作 m 束花。制作花束时,需要使用花园中 相邻的 k 朵花 。
花园中有 n 朵花,第 i 朵花会在 bloomDay[i] 时盛开,恰好 可以用于 一束 花中。
请你返回从花园中摘 m 束花需要等待的最少的天数。如果不能摘到 m 束花则返回 -1 。
示例 1:
输入:bloomDay = [1,10,3,10,2], m = 3, k = 1
输出:3
解释:让我们一起观察这三天的花开过程,x 表示花开,而 _ 表示花还未开。
现在需要制作 3 束花,每束只需要 1 朵。
1 天后:[x, _, _, _, _] // 只能制作 1 束花
2 天后:[x, _, _, _, x] // 只能制作 2 束花
3 天后:[x, _, x, _, x] // 可以制作 3 束花,答案为 3
示例 2:
输入:bloomDay = [1,10,3,10,2], m = 3, k = 2
输出:-1
解释:要制作 3 束花,每束需要 2 朵花,也就是一共需要 6 朵花。而花园中只有 5 朵花,无法满足制作要求,返回 -1 。
示例 3:
输入:bloomDay = [7,7,7,7,12,7,7], m = 2, k = 3
输出:12
解释:要制作 2 束花,每束需要 3 朵。
花园在 7 天后和 12 天后的情况如下:
7 天后:[x, x, x, x, _, x, x]
可以用前 3 朵盛开的花制作第一束花。但不能使用后 3 朵盛开的花,因为它们不相邻。
12 天后:[x, x, x, x, x, x, x]
显然,我们可以用不同的方式制作两束花。
示例 4:
输入:bloomDay = [1000000000,1000000000], m = 1, k = 1
输出:1000000000
解释:需要等 1000000000 天才能采到花来制作花束
示例 5:
输入:bloomDay = [1,10,2,9,3,8,4,7,5,6], m = 4, k = 2
输出:9
解答 :
java
class Solution {
public int minDays(int[] bloomDay, int m, int k) {
long l=Integer.MAX_VALUE;
long r=0;
if(bloomDay.length<(long)m*k) return -1;
for(int num:bloomDay){
l=Math.min(l,num);
r=Math.max(r,num);
}
while(l<r){
long mid=(r-l)/2+l;
boolean isOk=caculate(bloomDay,mid,m,k);
if (isOk){
r=mid;
}else{
l=mid+1;
}
}
return (int)l;
}
public boolean caculate(int[] bloomDay,long x1,int m,int k){
// int x=bloomDay[x1];
int []temp=new int[bloomDay.length];
for (int i=0;i<bloomDay.length;i++){
temp[i]=bloomDay[i];
}
int xiaoyux=0;
for(int i=0;i<temp.length;i++){
if(temp[i]<=x1){
xiaoyux++;
temp[i]=0;//表示花朵都开了
}
}
if(xiaoyux<m){ return false;
}
int currentZero=0;
int count=0;
for (int i=0;i<temp.length;i++){
if(temp[i]==0){
currentZero++;
}
if(currentZero>=k){
count++;
currentZero=0;
}
if(temp[i]!=0){
currentZero=0;
}
}
return count>=m;
}
}
### 爱吃香蕉的珂珂
珂珂喜欢吃香蕉。这里有 n 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,将在 h 小时后回来。
珂珂可以决定她吃香蕉的速度 k (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 k 根。如果这堆香蕉少于 k 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉。
珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。
返回她可以在 h 小时内吃掉所有香蕉的最小速度 k(k 为整数)。
示例 1:
输入:piles = [3,6,7,11], h = 8
输出:4
示例 2:
输入:piles = [30,11,23,4,20], h = 5
输出:30
示例 3:
输入:piles = [30,11,23,4,20], h = 6
输出:23
java
class Solution {
public int minEatingSpeed(int[] piles,int h){
int l=1;
int r=0;
Arrays.sort( piles);
r=piles[piles.length-1];
l=1;//每个小时吃一根~
while(l<r){
int mid=(r-l)/2+l;
// 计算时间
int temp=caculate(piles,mid);
if(temp>h){
// 证明mid 不能满足当前要求,mid偏低了
l=mid+1;
}else{
r=mid;
}
}
return l;
}
public int caculate(int[] piles,int k){
int time=0;
for(int i=0;i<piles.length;i++){
if(piles[i]<=k){
time++;
}else{
time+=(piles[i]+k-1)/k;
}
}
return time;
}
}
1011. 在 D 天内送达包裹的能力
提示
传送带上的包裹必须在 days 天内从一个港口运送到另一个港口。
传送带上的第 i 个包裹的重量为 weights[i]。每一天,我们都会按给出重量(weights)的顺序往传送带上装载包裹。我们装载的重量不会超过船的最大运载重量。
返回能在 days 天内将传送带上的所有包裹送达的船的最低运载能力。
示例 1:
输入:weights = [1,2,3,4,5,6,7,8,9,10], days = 5
输出:15
解释:
船舶最低载重 15 就能够在 5 天内送达所有包裹,如下所示:
第 1 天:1, 2, 3, 4, 5
第 2 天:6, 7
第 3 天:8
第 4 天:9
第 5 天:10
请注意,货物必须按照给定的顺序装运,因此使用载重能力为 14 的船舶并将包装分成 (2, 3, 4, 5), (1, 6, 7), (8), (9), (10) 是不允许的。
示例 2:
输入:weights = [3,2,2,4,1,4], days = 3
输出:6
解释:
船舶最低载重 6 就能够在 3 天内送达所有包裹,如下所示:
第 1 天:3, 2
第 2 天:2, 4
第 3 天:1, 4
示例 3:
输入:weights = [1,2,3,1,1], days = 4
输出:3
解释:
第 1 天:1
第 2 天:2
第 3 天:3
第 4 天:1, 1
java
class Solution {
public int shipWithinDays(int[] nums, int days) {
int l = 0;
int r = 0;
for (int num : nums) {
l = Math.max(l, num);
r += num;
}
while (l < r) {
int mid = (r - l) / 2 + l;
int k = caculate(nums, mid);
if(k > days){
l = mid + 1;
}else{
r = mid;
}
}
return l;
}
public int caculate(int[] nums,int mid){
// '
int days=0;
int sum = 0;
for(int i = 0; i < nums.length; i++){
sum+=nums[i];
if(sum > mid){
days++;
sum=nums[i];
}else if(sum==mid){
days++;
sum=0;
}
}
if(sum>0){
days++;
}
return days;
}
}
易错点:
总结:请注意,上述的题目都是最小化 !!!找最小满足条件的,所以k<=target r=mid;
但下面这个题是最大化!:
情况1:最大化最小值(像 LeetCode 1552 磁力球)
目标:让"最小的东西"(如间距)尽可能大!
例子:球间距最大化最小间距;跳跃游戏中最小跳跃距离最大化。
二分什么:二分那个"最小值 x"
检查函数:给定 x,计算"能做到多少"(如能放几个球)
判断:如果能做到 >= 目标数量 → x 可以更大!(往右走)
口诀:"能行就再大一点!"
情况2:最小化最大值(像 LeetCode 410 分割数组)
目标:让"最大的东西"(如子数组和)尽可能小!
例子:分割数组最小化最大和;船运货最小化最大船载重;资源分配最小化最大负载。
二分什么:二分那个"最大值 x"
检查函数:给定 x,计算"需要多少资源"(如需要几段)
判断:如果能用 <= 目标资源完成 → x 可以更小!(往左走)
口诀:"能行就再小一点!
在代号为 C-137 的地球上,Rick 发现如果他将两个球放在他新发明的篮子里,它们之间会形成特殊形式的磁力。Rick 有 n 个空的篮子,第 i 个篮子的位置在 position[i] ,Morty 想把 m 个球放到这些篮子里,使得任意两球间 最小磁力 最大。
已知两个球如果分别位于 x 和 y ,那么它们之间的磁力为 |x - y| 。
给你一个整数数组 position 和一个整数 m ,请你返回最大化的最小磁力。
1552 最大化最小磁力
示例 1:
输入:position = [1,2,3,4,7], m = 3
输出:3
解释:将 3 个球分别放入位于 1,4 和 7 的三个篮子,两球间的磁力分别为 [3, 3, 6]。最小磁力为 3 。我们没办法让最小磁力大于 3 。
示例 2:
输入:position = [5,4,3,2,1,1000000000], m = 2
输出:999999999
解释:我们使用位于 1 和 1000000000 的篮子时最小磁力最大。
提示:
n == position.length
2 <= n <= 10^5
1 <= position[i] <= 10^9
所有 position 中的整数 互不相同 。
2 <= m <= position.length
这个题目先排序!!!!caculate函数也是难点
java
class Solution {
public int maxDistance(int[] nums, int m) {
Arrays.sort(nums);
int l = 1;//最小磁力
int r = 1000000000;
int max=0;
int min=Integer.MAX_VALUE;
r=nums[nums.length - 1] - nums[0]+1;;
while (l < r) {
int mid = (r - l) / 2 + l;
// 为mid磁力的时候,需要的几个球?mid最小磁力为4的话,我需要两个球,那么我必须mid还得小!才可以需要的球多一些!
int k = caculate(nums, mid);
if (k>= m) {
// 最大化!!!
/**
经典错误写法(你之前就是这个坑!)
if (k > m) {
l = mid + 1; // 只在多放了才尝试更大
} else {
r = mid; // k == m 也往小了走 → 错!
}
这样写的话,在 k == m 时你就放弃了继续增大的机会,直接把答案压小了
*/
l = mid+1; // 能放 >=m 个,尝试更大间距
} else {
r = mid ; // 放不了 m 个,太大了
}
}
return l-1;
}
public int caculate(int[] nums,int mid){
// '
// 15 4
// 而为了"可行",我们必须做出 最有利于放更多球 的选择。
//
// 这个最有利(贪心)的策略就是:
//
// 球越靠左放越好。.
int sum=1;//至少一个球
int last=nums[0];
for (int i = 1; i < nums.length; i++){
if(nums[i]-last>=mid){
sum++;
last=nums[i];
}
}
return sum;
}
}
其他题目:
