410. Split Array Largest Sum
这是一道颠覆我算法大厦的革命性题,它的求解思路对我来说非常独特,是一道值得深入研究探讨的题目,单独拿出来分析下,若有帮助甚好,来源于leetcode二分搜索系列。
问题
Problems
Given an array which consists of non-negative integers and an integer m, you can split the array into m non-empty continuous subarrays. Write an algorithm to minimize the largest sum among these m subarrays.
Note:
If n is the length of array, assume the following constraints are satisfied:
- 1 ≤ n ≤ 1000
- 1 ≤ m ≤ min(50, n)
Examples:
Input:
nums = [7,2,5,10,8]
m = 2
Output:
18
Explanation:
There are four ways to split nums into two subarrays.
The best way is to split it into [7,2,5] and [10,8],
where the largest sum among the two subarrays is only 18.
题目意思是说,给你一个数组,然后一个m,把数组划分成m部分,前提是这m个子数组都是连续从原有数组中提取出来的,找出m部分最大的子数组和,并针对所有的可能划分情况,找到最小的最大的子数组和。
乍一看很难理解,没关系,举个例子很容易理解题意。如数组:
[7,2,5,10,8],m = 2
那么可以有几种划分使得变成两个数组?
1. [7],[2,5,10,8]
2. [7,2],[5,10,8]
3. [7,2,5],[10,8]
4. [7,2,5,10],[8]
对应于每个划分,我们可以分别求出每个划分中最大的子数组和,有:
1. 25
2. 23
3. 18
4. 24
所以应该返回18.
我的思路(可跳过)
如果直接按照上述的意思去写代码时,你会发现实操非常困难,所以必须对问题归简,这道题还可以这样理解,求最小的最大,关键在于最小,而不是最大!真正做约束的在于最小,针对每种划分情况,最大是每种划分的固有属性,所以完全可以不用考虑,否则增添理解题目的负担。那么最小的含义在于,对于
m=2
的情况,我们是尽可能的让左右两部分平衡,也就是说
sum(左数组) = sum(右数组)
,也就是平均分配。
先说说我的想法吧,但我的代码没有AC,后来发现,这种做法没法实现
m >= 3
的情况,太可惜了,但
m == 2
是完全适用的。
public int splitArray(int[] nums, int m) {
if (nums.length == 0) return 0;
long[] sums = new long[nums.length];
sums[0] = nums[0];
for (int i = 1; i < nums.length; i ++){
sums[i] = sums[i-1] + nums[i]; //sums 溢出
}
if (m == 1) return (int) sums[sums.length-1];
return (int) (sums[sums.length-1]-binarySearch(sums, sums[sums.length-1], m));
}
//m大于3的情况下,不一定max取在最后划分的那sum上
private long binarySearch(long[] sums, long maxSum, int m){
int lf = 0, rt = sums.length-1;
while (lf < rt){
int mid = lf + (rt + 1 - lf) / 2;
if(sums[mid] > maxSum / m){
rt = mid -1;
}else{
lf = mid;
}
}
if(sums[lf] <= maxSum / m) return sums[lf];
return sums[m-2];
}
首先求得累加和,因为累加和自然有序,所以可以以一种高效的方案搜索答案,二分查找。为了让两部分尽可能平衡,所以我们只需要找到最大的下标
i
,使得
sum[i] <= maxSum / 2
,自然能求得答案了。但上述思路是有问题的,按照这样的写法,最大和一定出现在右半部分,但其实可能在左半部分,那么用这种方法,难不成还要写两个二分么?自然无法适应
m >= 3
的情况。
正解思路
说它革新了我的世界观在于它的解决问题思路刷新了我对算法的认识,我的解法典型的给你
nums,m
想办法去求解
minMaxsum
,而大神的思路是假设我们已经在解空间里有了一系列
minMaxsums
,去搜索一个
minMaxsum
使得符合
m
,这让我非常震撼。
很简单,可能的minMaxsum有哪些,中间的哪些minMaxsum我们是不知道的,这是问题的关键!所以这个问题就假设最极端的两头情况:
- 当 m = 1 时,这种情况,minMaxsum = 整个数组之和。
- 当 m = 数组长度时,这种情况,minMaxsum = 数组中最大的那个元素。
如:[7,2,5,10,8] m = 1, 输出 minMaxsum = 32
而:[7,2,5,10,8] m = 5, 输出 minMaxsum = 10
所以我们有了两头的解!可以看成如下形式:
m | 5 | 4 | 3 | 2 | 1 |
---|---|---|---|---|---|
sum | 10 | ? | ? | ? | 32 |
有了如上形式,我们就可以去搜索可能的解了,这里需要注意的一个地方,10~32中的元素一定是不下降序列,这点很容易证明,题目已经做好了约束,所以它必然满足。现在这个问题就变成了给你
low = 10
,
high = 32
,假设m = 3,那么就是让你去找满足
m == 3
的情况下的“?”。这还不准确,应该说给你“?”情况下,找出符合
m==3
的情况。其中
? = (low + high) / 2
,一个二分!呵呵,的确有点让人奇怪,咱们继续构建这个解空间,
约束条件 | m | 5 | ? | ? | ? | ? | ? | ? | ? | ? | ? | 1 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
解空间 | sum | 10 | 11 | 12 | 13 | 14 | … | … | 29 | 30 | 31 | 32 |
所以,这样的构建,就解决了一个我的疑问,我们能确保所有可能的解都被搜索到么?一定可以,二分搜索在更新下标时都形如有
lf = mid + 1
,假设现在mid的物理含义是minMaxsum,那么它一定能把所有的10~32的解全部搜索一遍。
这里让我对二分查找有了重新的认识:
- 下标不一定是数组的下标,求解问题的解空间同样可以当作下标,满足状态递增就行。
- 一定要符合状态空间中元素的有序性么?不一定,只要在搜索时,能够有约束条件排除左半或者右半即可。
唉,这部分就是拼智商的时候了,往往找约束条件是最难的,于是问题变成了:
约束条件 | m | 5 | 4 | … | 4 | … | 3 | … | … | … | … | 1 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
解空间 | sum | 10 | 11 | … | 20 | … | Sum | … | 29 | 30 | 31 | 32 |
所以,现在我们的目标就是想办法让
lf
指针和
rt
指针narrow down到目标位置,先说说
lf
指针吧,这很容易理解,如果约束条件
m = 4
,而此时
sum = 20
,这说明数组还有余量narrow down到
m = 3
,因为
20 * 4 / 3 > 20
,而判断右指针就很简单了,如果在当前sum下能找出
m = 3
,我们不妨试试更小的sum。
public class Solution {
public int splitArray(int[] nums, int m) {
long sum = 0;
int max = 0;
for(int num: nums){
max = Math.max(max, num);
sum += num;
}
return (int)binary(nums, m, sum, max);
}
private long binary(int[] nums, int m, long high, long low){
long mid = 0;
while(low < high){
mid = (high + low)/2;
if(valid(nums, m, mid)){
high = mid;
}else{
low = mid + 1;
}
}
return high;
}
private boolean valid(int[] nums, int m, long max){
int cur = 0;
int count = 1;
for(int num: nums){
cur += num;
if(cur > max){
cur = num;
count++;
if(count > m){
return false;
}
}
}
return true;
}
}
搜索解空间,找对应的
m
,符合情况就输出,很好的一个思路。