力扣算法——二分最大值最小值

二分查找解法思路分析

  1. 分割数组的最大值

给定一个非负整数数组 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;
    }
}

其他题目:

相关推荐
仙俊红17 分钟前
LeetCode322零钱兑换
算法
颖风船20 分钟前
锂电池SOC估计的一种算法(改进无迹卡尔曼滤波)
python·算法·信号处理
551只玄猫33 分钟前
KNN算法基础 机器学习基础1 python人工智能
人工智能·python·算法·机器学习·机器学习算法·knn·knn算法
charliejohn34 分钟前
计算机考研 408 数据结构 哈夫曼
数据结构·考研·算法
POLITE342 分钟前
Leetcode 41.缺失的第一个正数 JavaScript (Day 7)
javascript·算法·leetcode
CodeAmaz1 小时前
一致性哈希与Redis哈希槽详解
redis·算法·哈希算法
POLITE31 小时前
Leetcode 42.接雨水 JavaScript (Day 3)
javascript·算法·leetcode
Tim_101 小时前
【算法专题训练】36、前缀树路径和
算法