410. 分割数组的最大值
思路:
虽然第一反应是用动态规划,可是想半天还是想不出动态方程,所以放弃了。。。
但是点开标签发现可以采用二分法来解题,但是我们的数组是无序的,所以如何确定二分法的思路呢?
我们可以首先采用贪心策略,先指定一个中间数mid,我们将数组不断划分成小于这个中间数的多个数组,划分方式使用贪心策略,也就是小于该数时不断往里放,直到满了就开辟另一个子数组再往里放。
由于此时我们并没有指定划分多少个,所以按照这种贪心策略可能划分的数组个数不等于题目要求的个数,如果我们划分的个数过大,我们就可以提高mid(增大子数组容量),相反如果我们划分的个数过小,我们就可以减小mid。
那么问题来了,如果我们划分的个数刚好等于题目要求个数,这时是该直接返回答案吗?当然不是!因为我们要求子数组和最小,所以我们还需要再缩小mid !!!
完整代码如下,细节见注释:
public int splitArray(int[] nums, int m) {
/*
二分法 //这部分参考自:https://leetcode-cn.com/problems/split-array-largest-sum/comments/
nums = [7,2,5,10,8]
m = 1,那么整个数组作为一部分,最小的最大值为 32
m = n,那么每个元素作为一个子数组,从所有元素选取最大值,最小的最大值小为 10
m 的取值范围为 1 <= m <= n,因此,最大值的最小值的范围为 [10, 32]
我们利用二分法查找,找出符合 m 的最大值的最小的结果
二分过程:
left = 10;
right = 32
mid = (left + right) >>> 1 = 21(这个 21 就是一个子数组的最大容量)
我们假设刚开辟的用来存储的子数组个数 cnt = 1
那么根据贪心思想,我们将数组元素按顺序逐个往里放
因此就有如下过程:
7 < 21
7 + 2 < 21
7 + 2 + 5 < 21
7 + 2 + 5 + 10 > 21
至此,我们可以看出一个 21 容量的子数组是无法容纳整个数组元素的,因此我们需要开辟第二个子数组来存储剩下的数组元素
cnt = cnt + 1 = 2
10 < 21
10 + 8 < 21
我们发现,两个子数组可以将整个数组元素放入,而 cnt 刚好等于 m,因此 [7,2,5] 和 [10,8] 就是分割出来的两个子数组,最小的最大值为 18
为什么是放入元素直到放不下为止?因为要求的是连续子数组,我们需要保证每个连续的子数组的元素和都尽可能的接近 21
如果我们最终得到的 cnt > m,那么表示我们划分出太多的子数组,也就是意味着一个子数组的容量太少,我们需要再扩大容量,即 left = mid + 1,然后继续进行二分
如果我们最终得到的 cnt < m,那么表示我们划分出太少的子数组,也就是意味着一个子数组的容量太大,需要减少容量,即 right = mid - 1
*/
long left = 0;//采用long是因为测试用例中出现了个别比较大的数
long right = 0;
for(int num : nums){
left = Math.max(left, num);
right += num;
}
//时间复杂度 O(nlogn)
while(left < right){
long mid = (left + right) >>> 1;
int c = 0;
long temp = 0;
for(int num : nums){
if(temp + num > mid){
c++;
temp = 0;
}
temp += num;
}
//最终需要再 +1 ,因为最后的一个数组在上面的循环中不会被添加
c++;
//分割的子数组个数比目标个数大,那么意味着容量小的,需要调大容量
if(c > m){
left = mid + 1;
}else{
//容量过大或者容量刚好,则缩小最大和
right = mid;
}
}
return (int)left;//right
}
之前我不理解的点在于为什么要返回left,因为我不确定left是否最终会收缩到最小和,后来我发现如果我们划分的个数刚好等于题目要求个数,mid会左移直到刚刚好能等于某个子数组和,所以最终left会收缩到和right一致,也就是会收缩到最终答案。
还有动态规划法,之后再更新。。。