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

二分查找解法思路分析

  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;
    }
}

其他题目:

相关推荐
monster000w3 小时前
大模型微调过程
人工智能·深度学习·算法·计算机视觉·信息与通信
小小晓.3 小时前
Pinely Round 4 (Div. 1 + Div. 2)
c++·算法
SHOJYS3 小时前
学习离线处理 [CSP-J 2022 山东] 部署
数据结构·c++·学习·算法
biter down3 小时前
c++:两种建堆方式的时间复杂度深度解析
算法
zhishidi3 小时前
推荐算法优缺点及通俗解读
算法·机器学习·推荐算法
WineMonk3 小时前
WPF 力导引算法实现图布局
算法·wpf
2401_837088503 小时前
双端队列(Deque)
算法
ada7_3 小时前
LeetCode(python)108.将有序数组转换为二叉搜索树
数据结构·python·算法·leetcode
奥特曼_ it4 小时前
【机器学习】python旅游数据分析可视化协同过滤算法推荐系统(完整系统源码+数据库+开发笔记+详细部署教程)✅
python·算法·机器学习·数据分析·django·毕业设计·旅游