算法---LeetCode 410. 分割数组的最大值

1. 题目

原题链接

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

示例 1:

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

输入:nums = [1,2,3,4,5], m = 2
输出:9

示例 3:

输入:nums = [1,4,4], m = 3
输出:4

提示:

1 <= nums.length <= 1000
0 <= nums[i] <= 106
1 <= m <= min(50, nums.length)

Related Topics 贪心 数组 二分查找 动态规划
👍 496 👎 0

2. 题解

2.1 解法1: 二分法

关键字分析:这个题目非常关键的字眼是:非负整数数组非空连续。尤其是「非负整数数组」和「连续」这两个信息

挖掘单调性:使用二分查找的一个前提是「数组具有单调性」,我们就去想想有没有单调性可以挖掘,不难发现:

  • 如果设置「数组各自和的最大值」很大,那么必然导致分割数很小;
  • 如果设置「数组各自和的最大值」很小,那么必然导致分割数很大。

注意事项:如果某个 数组各自和的最大值 恰恰好使得分割数为 m ,此时不能放弃搜索,因为我们要使得这个最大值 最小化,此时还应该继续尝试缩小这个 数组各自和的最大值 ,使得分割数超过 m ,超过 m 的最后一个使得分割数为 m数组各自和的最大值 就是我们要找的 最小值

算法流程

  1. 计算子数组的最大值范围, 即在区间 [max(nums),sum(nums)]
  2. left=max(nums), right=sum(sums), mid=(left+right)/2计算数组和最大值不大于mid对应的子数组个数 cnt(这个是关键!)
  3. 如果 cnt>m,说明划分的子数组多了,即我们找到的 mid 偏小,故left=mid+1
    否则,说明划分的子数组少了,即 mid 偏大(或者正好就是目标值),故 right=mid

简单的理解

我先猜一个mid值,然后遍历数组划分,使每个子数组和都最接近mid(贪心地逼近mid),这样我得到的子数组数一定最少;
如果即使这样子数组数量仍旧多于m个,那么明显说明我mid猜小了,因此 lo = mid + 1;
而如果得到的子数组数量小于等于m个,那么我可能会想,mid是不是有可能更小?值得一试。因此 hi = mid;

测试用例举例

例如:(题目中给出的示例)输入数组为 [7, 2, 5, 10, 8] ,m = 2 。如果设置 数组各自和的最大值 为 21,那么分割是 [7, 2, 5, | 10, 8],此时 m = 2,此时,这个值太大,尝试一点一点缩小:
设置 数组各自和的最大值 为 20,此时分割依然是 [7, 2, 5, | 10, 8],m = 2;
设置 数组各自和的最大值 为 19,此时分割依然是 [7, 2, 5, | 10, 8],m = 2;
设置 数组各自和的最大值 为 18,此时分割依然是 [7, 2, 5, | 10, 8],m = 2;
设置 数组各自和的最大值 为 17,此时分割就变成了 [7, 2, 5, | 10, | 8],这时 m = 3。
m 变成 3 之前的值 数组各自和的最大值 18 是这个问题的最小值,所以输出 18。

代码

    class Solution {
        public int splitArray(int[] nums, int m) {
            // 1. 计算子数组和的范围
            int maxVal = 0;
            int sumAll = 0;
            for (int i = 0; i < nums.length; i++) {
                maxVal = Math.max(maxVal, nums[i]);
                sumAll += nums[i];
            }

            int left = maxVal, right = sumAll;
            while (left < right) {
                // 注意这里可能存在数字超界问题, 建议使用位运算或 (right-left)/2 + left
                int mid = (right - left) / 2 + left;
                int sum = 0;
                int cnt = 1; // 至少有一个分割
                // 尝试加上当前遍历的这个数,如果加上去超过了「子数组各自的和的最大值」,就不加这个数,另起炉灶
                for (int i = 0; i < nums.length; i++) {
                    if (sum + nums[i] > mid) {
                        cnt++;
                        sum = 0;
                    }
                    sum += nums[i];
                }
                // 注意等于时的情况, 由于是求 满足m的子数组最大值中的最小值, 所以尝试减小 mid值, 向左收缩区间
                if (cnt <= m) {// 说明数字选大了, 需要减小
                    right = mid;
                } else {
                    left = mid + 1;
                }

            }
            return left;
        }
    }

参考:

二分查找

动态规划、二分查找(Java)

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
题目描述:给定一个非负整数数组nums一个整数m,你需要将这个数组分成m个非空连续数组。设计一个算法使得这m个数组中的最大和最小。 解题思路: 这是一个典型的二分搜索题目,可以使用二分查找来解决。 1. 首先确定二分的左右边界。左边界为数组中最大的值,右边界为数组中所有元素之和。 2. 在二分搜索的过程中,计算出分割数组的组数count,需要使用当前的中间值来进行判断。若当前的中间值不够分割成m个数组,则说明mid值偏小,将左边界更新为mid+1;否则,说明mid值偏大,将右边界更新为mid。 3. 当左边界小于等于右边界时,循环终止,此时的左边界即为所求的结果。 具体步骤: 1. 遍历数组,找到数组中的最大值,并计算数组的总和。 2. 利用二分查找搜索左右边界,从左边界到右边界中间的值为mid。 3. 判断当前的mid值是否满足题目要求,若满足则更新右边界为mid-1; 4. 否则,更新左边界为mid+1。 5. 当左边界大于右边界时,循环终止,返回左边界即为所求的结果。 代码实现: ```python class Solution: def splitArray(self, nums: List[int], m: int) -> int: left = max(nums) right = sum(nums) while left <= right: mid = (left + right) // 2 count = 1 total = 0 for num in nums: total += num if total > mid: total = num count += 1 if count > m: left = mid + 1 else: right = mid - 1 return left ``` 时间复杂度分析:二分搜索的时间复杂度为O(logN),其中N为数组的总和,而遍历数组的时间复杂度为O(N),因此总的时间复杂度为O(NlogN)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值