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
的 数组各自和的最大值 就是我们要找的 最小值。
算法流程
- 计算子数组的最大值范围, 即在区间
[max(nums),sum(nums)]
。 - 令
left=max(nums), right=sum(sums), mid=(left+right)/2
,计算数组和最大值不大于mid对应的子数组个数 cnt(这个是关键!) - 如果 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;
}
}
参考: