Q:给定一个非负整数数组和一个整数 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,在所有情况中最小。
A:
动态规划
首先我们把 \(f[i][j]\) 定义为将 \(nums[0..i]\) 分成 \(j\) 份时能得到的最小的分割子数组最大值。
对于第 \(j\) 个子数组,它为数组中下标 \(k + 1\) 到 \(i\) 的这一段。因此,\(f[i][j]\) 可以从 \(max(f[k][j - 1]\), \(nums[k + 1] + ... + nums[i])\) 这个公式中得到。遍历所有可能的 \(k\),会得到 \(f[i][j]\) 的最小值。
整个算法那的最终答案为 \(f[n][m]\),其中 \(n\) 为数组大小。
public int splitArray(int[] nums, int m) {
int n = nums.length;
int[][] f = new int[n + 1][m + 1];
int[] sub = new int[n + 1];
for (int i = 0; i <= n; i++) {
Arrays.fill(f[i], Integer.MAX_VALUE);
}
for (int i = 0; i < n; i++) {
sub[i + 1] = sub[i] + nums[i];
}
f[0][0] = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
for (int k = 0; k < i; k++) {
f[i][j] = Math.min(f[i][j], Math.max(f[k][j - 1], sub[i] - sub[k]));
}
}
}
return f[n][m];
}
二分法+贪心
我们很容易就能找到这个答案的一个性质:
如果我们找到了一种分割方案,使得最大的分割子数组和不超过 x,那么我们也能找到一种分割方案使得最大的分割子数组和不超过 y,其中 y 大于 x。
对于值 x,我们把这个性质定义为 F(x)。如果 F(x) 为真,那就意味着我们一定可以找到一种分割方案使得最大的分割子数组和不超过 x。我们让 x 的区间为 负无穷大 到 无穷大,一旦我们找到一个值 x0,使得所有的 x < x0,F(x) 都为假,所有的 x >= x0,F(x) 都为真。那么显然,这个 x0 就是我们要的答案了。
我们可以用二分搜索来找到 x0。每次循环,我们让 mid = (left + right) / 2,如果 F(mid) 为假,那么我们接下来就去搜索 [mid + 1, right] 区间;如果 F(mid) 为真,那么我们接下来就去搜索 [left, mid - 1] 区间。
对于一个给定的 x,我们可以用贪心算法来计算 F(x)。我们用累计和 sum 来记录当前子数组的和,同时用 cnt 来记录子数组的数量。我们依次处理数组中的每个元素,对每个元素 num,如果 sum + num <= x,这就意味着当前的子数组没有超过限制。否则,就需要从当前元素 num 开始分割出一个新的子数组。
在完成遍历完整个数组之后,比较 cnt 和 m 的值。如果 cnt <= m,这就意味可以找到一种分割方案使得最大的分割子数组和不超过 x。否则,F(x) 一定为假。
代码:
public int splitArray(int[] nums, int m) {
long l = 0;//下限
long r = 0;//上限
int n = nums.length;
for (int i = 0; i < n; i++) {
r += nums[i];//l为整个数组的总数
if (l < nums[i]) {
l = nums[i];//l为单个的最大值
}
}
long res = r;
while (l <= r) {
long mid = (l + r) / 2;
long sum = 0;//子数组总数
int count = 1;//分割数组数量
for (int i = 0; i < n; i++) {
if (sum + nums[i] > mid) {//超过阈值了
count++;
sum = nums[i];//重新分割
} else {
sum += nums[i];//继续加进去
}
}
if (count <= m) {
res = mid;//这种情况可分
r = mid - 1;//继续查找更小的mid值
} else {
l = mid + 1;//这种情况不可分,mid值过小了
}
}
return (int) res;
}