难度困难
给定一个非负整数数组和一个整数 m,你需要将这个数组分成 m 个非空的连续子数组。设计一个算法使得这 m 个子数组各自和的最大值最小。
注意:
数组长度 n 满足以下条件:
- 1 ≤ n ≤ 1000
- 1 ≤ m ≤ min(50, n)
示例:
输入:
nums = [7,2,5,10,8]
m = 2
输出:
18
解释:
一共有四种方法将nums分割为2个子数组。
其中最好的方式是将其分为[7,2,5] 和 [10,8],
因为此时这两个子数组各自的和的最大值为18,在所有情况中最小。
解法1:二分法
class Solution {
int[] numArr;
int length;
/*
二分查找的思路
即我们要在一个范围内,查找我们想要的这个值capacity
二分查找的范围
要进行二分查找,首先要找到一个范围
你觉得这个capacity最小能多小? 整个数组的最大值
你觉得这个capacity最大能多大? 整个数组的和
所以二分查找的范围是[max(数组), sum(数组)]
进行二分查找
二分查找本质上就是每次测试范围的中点(capacity)是给大了还是给小了
从而缩小查找的范围
如何看这个中点capacity值 是给大了 还是给小了?
每次:
假设这个中点mid(capacity)就是 “d个子数组各自和的最大值中” 的最小值
那么每一个子数组和必定 <= mid(capacity)
你用这个capacity值来对数组进行从头分割,
一旦当前子数组和 > mid(capacity),就结束该数组,重新开启一个新的子数组
如果你用这个mid(capacity)创建的子数组数量,比d还多,
说明你这个capacity值定小了,所以二分查找取哪一半?右半!
如果你用这个mid(capacity)创建的子数组数量,比d少了,
说明你这个capacity值定大了,所以二分查找取哪一半?左半!
*/
public int splitArray(int[] nums, int d) {
numArr = nums;
length = nums.length;
// 二分查找: 左边界
int left = getMaxCapacity();
// 二分查找: 右边界 + 1
int right = getSumCapacity();
while (left < right) {
int mid = left + (right - left) / 2;
// 如果numArr,根据mid容量 划分出来的子数组个数 <= d,说明:容量偏大,还可以减少容量
if (check(numArr, mid, d)) {
right = mid;
} else {
left = mid + 1;
}
}
// 因为求的是 最大值的最小值,所以返回左边界
return left;
}
// 辅助函数1: 数组中最大元素
public int getMaxCapacity() {
int tmpMax = 0;
for (int i = 0; i < length; i++) {
if (numArr[i] > tmpMax) {
tmpMax = numArr[i];
}
}
return tmpMax;
}
// 辅助函数2: 数组和
public int getSumCapacity() {
int sum = 0;
for (int i = 0; i < length; i++) {
sum += numArr[i];
}
return sum;
}
// 验证:numArr 在给定的capacity下,划分出的group是不是小于d
public boolean check(int[] numArr, int capacity, int d) {
int sum = 0;
int group = 1;
for (int i = 0; i < numArr.length; i++) {
// 如果超出容量
if (sum + numArr[i] > capacity) {
// 再开新的一堆(开个新的子数组)
group++;
// 为下一堆做准备
sum = numArr[i];
} else {
sum += numArr[i];
}
}
// 如果堆数(子数组个数) <= d,说明给的capcity大了,还可以减少容量
return group <= d;
}
}