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:
Given m satisfies the following constraint: 1 ≤ m ≤ length(nums) ≤ 14,000.Examples:
Input:
nums = [7,2,5,10,8]
m = 2Output:
18Explanation:
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.
题意大概就是,给定一个序列nums以及一个整数m,要求把序列nums分成m份,并且要让这m个子序列各自的和的最大值最小(minimize the largest sum among these m subarrays)。
这道题我是在动态规划的分类里面找的,所以一开始也是想用动态规划。
大致思路是考虑一个序列最右边切割的位置。在这里arr[n][m]的意思是对序列arr[1…n]切割m-1次(即划分成m块)的最好结果。
这里有, arr[n][m]=minkmax(arr[k][m−1],arr[n][1]−arr[k][1]) ,后面这个 arr[n][1]−arr[k][1] 就是第k+1个数到第n个数的累加,其实就是第m块的和了。
class Solution {
public:
int splitArray(vector<int> &nums, int m) {
//corner cases...
vector<vector<int>> arr(nums.size(), vector<int>(m + 1, INT_MAX));
//m == 1
arr[0][1] = nums[0];
for (int i = 1; i < nums.size(); i++) {
arr[i][1] = arr[i - 1][1] + nums[i];
}
//m > 1
for (int m_i = 2; m_i <= m; m_i++) {
for (int n_j = m_i - 1; n_j < nums.size(); n_j++) {
arr[n_j][m_i] = INT_MAX;
for (int k = 0; k < n_j; k++) {
arr[n_j][m_i] = min(arr[n_j][m_i], max(arr[k][m_i - 1], arr[n_j][1] - arr[k][1]));
}
}
}
return arr[nums.size() - 1][m];
}
};
这个结果应该是对的,我验算过一些测试案例。但很不幸啊,TLE了。我想要不就是我这个DP太挫,还有更好的想法,要不就是这题目分类就是误导人的。
后来我参考了Discuss上的一些帖子,其中这篇我觉得是最浅显易懂的,写得很清楚,目测也是国人。
[C++ / Fast / Very clear explanation / Clean Code] Solution with Greedy Algorithm and Binary Search)
核心思路就是,我们观察到对于一个序列nums分割成m块,无论怎么分割,这个答案(最小值)一定在区间[max(nums), sum(nums)]之中。
比如[1,2,3,4],最好的情况是[[1],[2],[3],[4]] (你可以随意切),最坏的情况是[[1,2,3,4]] (一刀也不能切)。考虑m块,也就是m-1个切割,切割的结果无论好坏总是在上述这个区间里面。
但我们要的是最好的结果。假如可以分成m>=size(nums)块,显而易见,我们就能够取到区间最小值max(nums),但m要是小一些的话就不一定了。比如[1,2,3,4],m=2,我们不能切成两块并使得结果为4,显然这里结果是6。
那我们就只能从区间最小值开始搜索了。比如上面这个例子,我们尝试4,看看能不能找到一种切割,使得每个块的和都不大于4,这做不到吧;于是尝试5,不行;尝试6,可以了,[1,2,3],[4],那么6一定就是最后的结果。
但这样做太慢了,我们可以用二分搜索。假设我们已经构造了一个检测是否存在一个切割使得最大值为k的划分的函数(即下面代码中的doable()
),我们可以在区间[max(nums),sum(nums)]中二分查找。因为区间符合的数肯定符合[false, false, …, false, true, … true]这种结构的,我们通过二分查找来找最小的这个true就好了。
因为作者已经提供了一份C++的代码,这里我就改用Python吧。
import math
class Solution(object):
def doable(self, nums, m, k):
acc = 0
for n in nums:
if acc + n > k:
if m == 1:
return False
acc = 0
m -= 1
acc = acc + n
return True
def splitArray(self, nums, m):
lo = max(nums)
hi = sum(nums)
while lo < hi:
mid = math.floor((lo + hi) / 2);
if self.doable(nums, m, mid):
hi = mid - 1
else:
lo = mid + 1
if self.doable(nums, m, lo):
return int(lo)
else:
return int(lo + 1)