题目详情
给定一个非负整数数组和一个整数 m,你需要将这个数组分成 m 个非空的连续子数组。设计一个算法使得这 m 个子数组各自和的最大值最小。
注意:
数组长度 n 满足以下条件:
示例:
输入:
nums = [7,2,5,10,8]
m = 2
输出:
18
解释:
一共有四种方法将nums分割为2个子数组。
其中最好的方式是将其分为[7,2,5] 和 [10,8],
因为此时这两个子数组各自的和的最大值为18,在所有情况中最小。
——题目难度:困难
「将数组分割为 m 段,求……」是动态规划题目常见的问法,得记住。
依据题意,可以使用一个 dp 二维数组,dp[i][j] 来表示 将数组 nums 的前 i 个数分割为 j 段所能得到最大连续子数组和的最小值。再使用一个 sub 数组,sub[i] 表示数组 nums 的前 i 个数之和。
状态转移时,固定好 i 后( i 为右边界),考虑第 j 段的具体范围,然后来枚举 k,其中前 k 个数被分割为 j - 1 段,而第 k + 1 到第 i 个数为第 j 段,因此,第 j 段子数组中和的最大值 就等于 dp[k][k - 1] 与 sub[i] - sub[k] 中的较大值,然后再不断遍历 k 的过程中,刷新 dp[i][j],以此来得到 最小值的 dp[i][j]。
接下来确定边界条件 和 初始情况。
1.就在一开始,当 i = 1,j = 1时,唯一的可能性就是前 i 个数被分到了一段(k = 0),那么要求
max(dp[0][0], sub[1] - sub[0]) 的结果要为 sub[1] - sub[0],因此 dp[0][0] 得初始化为 0
也就是当 j = 1,k ≠ 0时,dp[k][0] 是不合法的状态,又由于是由 dp[0][0] 所推导上去的,所以也说明 dp[0][0] 得初始化为 0
2.对于状态 dp[i][j],当 i < j 是不合法的,由于目标是求出最小值,因此可以先把这些状态初始化为一个很大的数(dp[0][0] 除外)。这样一来,在状态转移的过程中,当出现 f[k][j - 1] (j - 1 > k) 时, max(...)将是一个很大的数,也就不会对最外层的 min(...) 产生影响。
-下面代码
class Solution {
public:
int splitArray(vector& nums, int m) {
int n = nums.size();
vector> dp(n + 1, vector(m + 1, LONG_LONG_MAX));
vector sub(n + 1, 0);
for(int i = 0; i < n; i++) {
sub[i + 1] = sub[i] + nums[i];
}
dp[0][0] = 0;
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= min(i, m); j++) {
for(int k = 0; k < i; k++) {
dp[i][j] = min(dp[i][j], max(dp[k][j - 1], sub[i] - sub[k]));
}
}
}
return dp[n][m];
}
};
「使……最大值尽可能小」是二分搜索题目常见的问法。
依据题意,子数组的最大值是有范围的,即在区间 [max(nums),sum(nums)] 之中。
令 left = max(nums),right = sum(nums),mid = (l + h) / 2,
计算数组和最大值不大于 mid 对应的子数组个数 cnt(这里用到了贪心解法:猜好一个 mid 值后,然后遍历数组划分,使每个子数组和都最接近 mid(贪心地逼近 mid),这样得到的子数组数一定最少。
如果即使是这样子数组的数量仍然多于 m 个,那么显然说明这次 mid 猜小了,因此 left = mid + 1;
那如果是这样得到的子数组数量正好为 m 个,那么 mid 是否可以再小一点呢?可以试试看,所以 right = mid;
如果这样得到的子数组数量小于 m 个,那么说明 mid 取大了,所以得缩小 mid,所以 right = mid)
通过二分查找,不断缩小搜索区间,最后可以得到的区间里只有一个元素(left = right),也就是得到最小的最大分割子数组和,这样就可以得到最终的答案了。
-下面代码
class Solution {
public:
int splitArray(vector& nums, int m) {
long long left = nums[0], right = 0;
for(int num : nums) {
left = num > left ? num : left;
right += num;
}
while (left < right) {
long long mid = (left + right) / 2;
long long temp = 0;
int cnt = 1;
for(int num : nums) {
temp += num;
if (temp > mid) {
temp = num;
cnt++;
}
}
if (cnt > m) {
left = mid + 1;
} else { //cnt <= m
right = mid;
}
}
return left;
}
};