public static int bestSplit0(int[] arr) {
if (arr == null || arr.length < 2) return 0;
int ans = 0, n = arr.length;
// 长度n数组有n-1种划分方式
for (int i = 0; i < n - 1; i++) {
// 分别计算每一种划分方式下的左右两部分的累加和
int sumL = 0, sumR = 0;
for (int l = 0; l <= i; l++) sumL += arr[l];
for (int r = i + 1; r < n; r++) sumR += arr[r];
// 收集答案
ans = Math.max(ans, Math.min(sumL, sumR));
}
return ans;
}
public static int bestSplit(int[] arr) {
if (arr == null || arr.length < 2) return 0;
// 先预处理计算出整个数组的累加和
int n = arr.length, sum = 0, ans = 0, sumL = 0;
for (int x : arr) sum += x;
// 依次遍历每一种划分方式,计算左部分累加和、右部分累加和 = 总累加和 - 左部分累加和
for (int i = 0; i < n - 1; i++) {
sumL += arr[i];
ans = Math.max(ans, Math.min(sumL, sum - sumL));
}
return ans;
}
public static int[] bestSplit0(int[] arr) {
if (arr == null || arr.length == 0) return new int[0];
int n = arr.length;
int[] ans = new int[n];
for (int range = 1; range < n; range++) {
for (int i = 0; i < range; i++) {
int sumL = 0, sumR = 0;
for (int l = 0; l <= i; l++) sumL += arr[l];
for (int r = i + 1; r <= range; r++) sumR += arr[r];
ans[range] = Math.max(ans[range], Math.min(sumL, sumR));
}
}
return ans;
}
public static int[] bestSplit(int[] arr) {
if (arr == null || arr.length == 0) return new int[0];
int n = arr.length;
int[] ans = new int[n], sum = new int[n + 1];
for (int i = 0; i < n; i++) sum[i + 1] = sum[i] + arr[i];
for (int range = 1; range < n; range++) {
for (int i = 0; i < range; i++) {
int sumL = rangeSum(sum, 0, i), sumR = rangeSum(sum, i + 1, range);
ans[range] = Math.max(ans[range], Math.min(sumL, sumR));
}
}
return ans;
}
private static int rangeSum(int[] sum, int l, int r) {
return sum[r + 1] - sum[l];
}
public static int[] bestSplitOptimal(int[] arr) {
if (arr == null || arr.length == 0) return new int[0];
int n = arr.length;
int[] ans = new int[n], sum = new int[n + 1];
for (int i = 0; i < n; i++) sum[i + 1] = sum[i] + arr[i];
// 最优划分 0~range-1上,最优划分是左部分[0, best] 右部分[best+1, range-1]
int best = 0;
for (int range = 1; range < n; range++) {
while (best + 1 < range) {
int before = Math.min(rangeSum(sum, 0, best), rangeSum(sum, best + 1, range));
int after = Math.min(rangeSum(sum, 0, best + 1), rangeSum(sum, best + 2, range));
if (after >= before) best++;
else break;
}
ans[range] = Math.min(rangeSum(sum, 0, best), rangeSum(sum, best + 1, range));
}
return ans;
}
public static int minStoneMerge0(int[] arr) {
if (arr == null || arr.length < 2) return 0;
int[] sum = getPrefixSum(arr);
return process(sum, 0, arr.length - 1);
}
// 返回 l~r 范围进行相邻石子合并得分的最小值
private static int process(int[] sum, int l, int r) {
if (l == r) return 0;
int next = Integer.MAX_VALUE;
for (int leftEnd = l; leftEnd < r; leftEnd++) {
next = Math.min(next, process(sum, l, leftEnd) + process(sum, leftEnd + 1, r));
}
return next + rangeSum(sum, l, r);
}
// 前缀和数组
private static int[] getPrefixSum(int[] arr) {
int n = arr.length;
int[] sum = new int[n + 1];
for (int i = 0; i < n; i++) sum[i + 1] = sum[i] + arr[i];
return sum;
}
// 区间和
private static int rangeSum(int[] sum, int l, int r) {
return sum[r + 1] - sum[l];
}
public static int minStoneMerge(int[] arr) {
if (arr == null || arr.length < 2) return 0;
int n = arr.length;
int[] sum = getPrefixSum(arr);
int[][] dp = new int[n][n];
// 左下半边区域无效,对角线 dp[i][i] = 0;
// 整体从下至上(n-2 ~ 0),从左至右(l ~ n) 填写dp表
for (int l = n - 2; l >= 0; l--) {
for (int r = l + 1; r < n; r++) {
int next = Integer.MAX_VALUE;
// 枚举每一个划分位置
for (int leftEnd = l; leftEnd < r; leftEnd++) {
next = Math.min(next, dp[l][leftEnd] + dp[leftEnd + 1][r]);
}
dp[l][r] = next + rangeSum(sum, l, r);
}
}
return dp[0][n - 1];
}
public static int minStoneMergeOptimal(int[] arr) {
if (arr == null || arr.length < 2) return 0;
int n = arr.length;
int[] sum = getPrefixSum(arr);
// best: 记录取得最优划分点的位置
int[][] dp = new int[n][n], best = new int[n][n];
// 填写倒数第2条斜对角线的dp表和best表
for (int i = 0; i < n - 1; i++) {
best[i][i + 1] = i;
dp[i][i + 1] = rangeSum(sum, i, i + 1);
}
for (int l = n - 3; l >= 0; l--) {
for (int r = l + 2; r < n; r++) {
int next = Integer.MAX_VALUE, choose = -1;
// 针对方法二`枚举每一个划分位置`进行优化: 利用best数组限制leftEnd的边界(左边, 下边)
for (int leftEnd = best[l][r - 1]; leftEnd <= best[l + 1][r]; leftEnd++) {
int cur = dp[l][leftEnd] + dp[leftEnd + 1][r];
if (cur <= next) {
next = cur;
// 记下此时取得最优的位置
choose = leftEnd;
}
}
best[l][r] = choose;
dp[l][r] = next + rangeSum(sum, l, r);
}
}
return dp[0][n - 1];
}
给定一个整型数组 arr,数组中的每个值都为正数,表示完成一幅画作需要的时间,再给定一个整数num,表示画匠的数量,每个画匠只能画连在一起的画作。所有的画家并行工作,返回完成所有的画作需要的最少时间。
测试链接:https://leetcode.cn/problems/split-array-largest-sum/
方法一:不优化枚举的动态规划方法,时间复杂度O(N^2 * K)
public static int splitArray(int[] nums, int k) {
int n = nums.length;
int[] sum = getPrefixSum(nums);
// dp[i][j]: 0~i幅画交给j个画匠完成的最短时间
int[][] dp = new int[n][k + 1];
// 初始化dp表,第0列(没有画匠)无效
// 第0行: 0~0,很显然一幅画,最短时间就是nums[0]
for (int j = 1; j <= k; j++) dp[0][j] = nums[0];
// 第1列: 很显然1个画匠完成0~i幅画,最短时间就是arr[0...i]累加和
for (int i = 1; i < n; i++) dp[i][1] = rangeSum(sum, 0, i);
// 其它位置: 从上往下、从左往右 递推
for (int i = 1; i < n; i++) {
for (int j = 2; j <= k; j++) {
int ans = Integer.MAX_VALUE;
// 枚举每一个划分位置(不进行优化)
for (int leftEnd = 0; leftEnd < i; leftEnd++) {
ans = Math.min(ans, Math.max(dp[leftEnd][j - 1], rangeSum(sum, leftEnd + 1, i)));
}
dp[i][j] = ans;
}
}
return dp[n - 1][k];
}
// 前缀和数组
private static int[] getPrefixSum(int[] arr) {
int n = arr.length;
int[] sum = new int[n + 1];
for (int i = 0; i < n; i++) sum[i + 1] = sum[i] + arr[i];
return sum;
}
// 区间和
private static int rangeSum(int[] sum, int l, int r) {
return sum[r + 1] - sum[l];
}
public static int splitArray(int[] nums, int k) {
int n = nums.length;
int[] sum = getPrefixSum(nums);
// best: 记录取得最优划分点的位置
int[][] dp = new int[n][k + 1], best = new int[n][k + 1];
// 初始化dp表,第0列(没有画匠)无效
// 第0行: 0~0,很显然一幅画,最短时间就是nums[0],最优划分点 -1
for (int j = 1; j <= k; j++) {
dp[0][j] = nums[0];
best[0][j] = -1;
}
// 第1列: 很显然1个画匠完成0~i幅画,最短时间就是arr[0...i]累加和,最优划分点 -1
for (int i = 1; i < n; i++) {
dp[i][1] = rangeSum(sum, 0, i);
best[i][1] = -1;
}
// 其它位置: 从左往右、从下往上 递推
// 为什么这样的顺序?因为要去凑(左,下)优化位置对儿!
for (int j = 2; j <= k; j++) {
for (int i = n - 1; i >= 1; i--) {
int ans = Integer.MAX_VALUE, bestChoose = -1;
// 如果i==N-1,则不优化上限
int down = best[i][j - 1], up = i == n - 1 ? n - 1 : best[i + 1][j];
// 使用四边形不等式优化枚举位置
for (int leftEnd = down; leftEnd <= up; leftEnd++) {
int leftCost = leftEnd == -1 ? 0 : dp[leftEnd][j - 1];
int rightCost = leftEnd == i ? 0 : rangeSum(sum, leftEnd + 1, i);
int cur = Math.max(leftCost, rightCost);
/**
* 注意下面的if一定是 < 课上的错误就是此处!当时写的 <=
* 也就是说,只有取得明显的好处才移动,举个例子来说明,比如[2,6,4,4],3个画匠时候,如下两种方案都是最优
* (2,6) (4) 两个画匠负责 | (4) 最后一个画匠负责;(2,6) (4,4)两个画匠负责 | 最后一个画匠什么也不负责
* 第一种方案划分为:[0~2] [3~3];第二种方案划分为:[0~3] [无]
* 两种方案的答案都是8,但是划分点位置一定不要移动,只有明显取得好处时(<),划分点位置才移动
* 也就是说后面的方案如果==前面的最优,不要移动!只有优于前面的最优,才移动
* 比如上面的两个方案,如果你移动到了方案二,你会得到:[2,6,4,4] 三个画匠时,最优为[0~3](前两个画家) [无](最后一个画家),
* 最优划分点为3位置(best[3][3]),那么当4个画匠时,也就是求解dp[3][4]时,因为best[3][3] = 3,这个值提供了dp[3][4]的下限
* 而事实上dp[3][4]的最优划分为:[0~2](三个画家处理) [3~3] (一个画家处理),此时最优解为6
* 所以,你就得不到dp[3][4]的最优解了,因为划分点已经越过2了
* 提供了对数器验证,你可以改成<=,对数器和leetcode都过不了,这里是<,对数器和leetcode都能通过
* 这里面会让同学们感到困惑的点:为啥==的时候,不移动,只有<的时候,才移动呢?例子懂了,但是道理何在?
* 哈哈哈哈哈,看了邮局选址问题,你更懵,请看42节!
*/
if (cur < ans) {
ans = cur;
bestChoose = leftEnd;
}
}
dp[i][j] = ans;
best[i][j] = bestChoose;
}
}
return dp[n - 1][k];
}
public static int splitArrayOptimal(int[] nums, int k) {
// 先计算nums数组的累加和
long sum = 0;
for (int x : nums) sum += x;
// 在0~sum上做二分
long l = 0, r = sum, mid, cur, ans = 0;
while (l <= r) {
mid = (l + r) / 2;
cur = getNeedParts(nums, mid);
if (cur <= k) {
ans = mid;
r = mid - 1;
} else l = mid + 1;
}
return (int) ans;
}
// 达成目标aim时,返回至少需要几个画匠
private static int getNeedParts(int[] nums, long aim) {
for (int x : nums) {
if (x > aim) return Integer.MAX_VALUE;
}
int ans = 1, sum = nums[0];
for (int i = 1; i < nums.length; i++) {
// 如果超了目标aim,就新加一个画匠,否则累加到sum
if (sum + nums[i] > aim) {
ans++;
sum = nums[i];
} else sum += nums[i];
}
return ans;
}
预处理一个结构:一张 w 表,w[i][j] 返回 [i, j] (i < j)范围只建一个邮件时的最短距离。
结论:在一个有序数组中,如果只建一个邮件,那么选择中点(奇数:中点、偶数:上中点或下中点都行)位置一定是最优的。(不用管数据多么倾斜)
public static int minDistance(int[] arr, int num) {
if (arr == null || arr.length < num || num < 1) return 0;
int n = arr.length;
// 预处理结构:w[i][j] 返回 [i, j] (i < j)范围只建一个邮件时的最短距离
// w多准备一个长度,避免边界值处理
int[][] w = new int[n + 1][n + 1];
// 初始化w数组,只填右上半区域(从上往下、从左往右)
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
w[i][j] = w[i][j - 1] + (arr[j] - arr[(i + j) >> 1]);
}
}
int[][] dp = new int[n][num + 1];
// 初始化dp数组,第0行(即0个居民点,显然都是0,不用填),第0列(即0个邮件,无效不用填)
// 第1列直接从w数组拿值
for (int i = 0; i < n; i++) dp[i][1] = w[0][i];
for (int i = 1; i < n; i++) {
// 优化 Math.min(i, num),即邮件点个数如果超过居民点个数,最短距离一定是0(每个居民点都可以建一个邮件),不用求
for (int j = 2; j <= Math.min(i, num); j++) {
int ans = Integer.MAX_VALUE;
// 不做优化,枚举每一个划分位置
for (int k = 0; k <= i; k++) {
ans = Math.min(ans, dp[k][j - 1] + w[k + 1][i]);
}
dp[i][j] = ans;
}
}
return dp[n - 1][num];
}
public static int minDistanceOptimal(int[] arr, int num) {
if (arr == null || arr.length < num || num < 1) return 0;
int n = arr.length;
// 预处理结构:w[i][j] 返回 [i, j] (i < j)范围只建一个邮件时的最短距离
// w多准备一个长度,避免边界值处理
int[][] w = new int[n + 1][n + 1];
// 初始化w数组,只填右上半区域(从上往下、从左往右)
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
w[i][j] = w[i][j - 1] + (arr[j] - arr[(i + j) >> 1]);
}
}
int[][] dp = new int[n][num + 1];
int[][] best = new int[n][num + 1];
// 初始化dp数组,第0行(即0个居民点,显然都是0,不用填),第0列(即0个邮件,无效不用填)
// 第1列直接从w数组拿值
for (int i = 0; i < n; i++) {
dp[i][1] = w[0][i];
best[i][1] = -1;
}
for (int j = 2; j <= num; j++) {
for (int i = n - 1; i >= j; i--) {
int down = best[i][j - 1], up = i == n - 1 ? n - 1 : best[i + 1][j];
int ans = Integer.MAX_VALUE, bestChoose = -1;
for (int leftEnd = down; leftEnd <= up; leftEnd++) {
int leftCost = leftEnd == -1 ? 0 : dp[leftEnd][j - 1];
int rightCost = leftEnd == i ? 0 : w[leftEnd + 1][i];
int cur = leftCost + rightCost;
// 这里 <= 或 < 都对
if (cur <= ans) {
ans = cur;
bestChoose = leftEnd;
}
}
dp[i][j] = ans;
best[i][j] = bestChoose;
}
}
return dp[n - 1][num];
}
public static int superEggDrop0(int k, int n) {
if (k < 1 || n < 1) return 0;
return process(n, k);
}
// 还剩k个鸡蛋去验证level层楼,返回至少需要扔几次
private static int process(int level, int k) {
if (level == 0) return 0;
// 只有1个鸡蛋时,只能依次从下往上每层楼都去试
if (k == 1) return level;
int min = Integer.MAX_VALUE;
for (int i = 1; i <= level; i++) {
// 1、第i层鸡蛋碎了,则还剩k-1个鸡蛋去下面的i-1层尝试
// 2、第i层鸡蛋没碎,则还剩k个鸡蛋去上面的level-i层尝试
min = Math.min(min, Math.max(process(i - 1, k - 1), process(level - i, k)));
}
return min + 1;
}
public static int superEggDrop(int k, int n) {
if (k < 1 || n < 1) return 0;
if (k == 1) return n;
// dp[i][j]: i层楼有j个鸡蛋最多扔几次
int[][] dp = new int[n + 1][k + 1];
// 初始化dp,第0行(即0层楼不用尝试)都是0,第0列(即0个鸡蛋)无效
// 第1列(即1个鸡蛋,则最差情况有多少层楼就得尝试多少次)
for (int i = 1; i <= n; i++) dp[i][1] = i;
// 普遍位置填写dp表,从上往下、从左往右
for (int i = 1; i <= n; i++) {
for (int j = 2; j <= k; j++) {
int min = Integer.MAX_VALUE;
// 枚举每一种情况
for (int i0 = 1; i0 <= i; i0++) {
min = Math.min(min, Math.max(dp[i0 - 1][j - 1], dp[i - i0][j]));
}
dp[i][j] = min + 1;
}
}
return dp[n][k];
}
public static int superEggDropOptimize(int k, int n) {
if (k < 1 || n < 1) return 0;
if (k == 1) return n;
// dp[i][j]: i层楼有j个鸡蛋最多扔几次
int[][] dp = new int[n + 1][k + 1];
int[][] best = new int[n + 1][k + 1];
// 初始化dp,第0行(即0层楼不用尝试)都是0,第0列(即0个鸡蛋)无效
// 第1列(即1个鸡蛋,则最差情况有多少层楼就得尝试多少次)
for (int i = 1; i <= n; i++) dp[i][1] = i;
// 第1行(即1层楼,不管有几个鸡蛋都只需要尝试1次)
for (int i = 1; i <= k; i++) {
dp[1][i] = 1;
best[1][i] = 1;
}
// 普遍位置填写dp表,从上往下、从右往左
for (int i = 2; i <= n; i++) {
for (int j = k; j > 1; j--) {
// 依赖上边作为下限、右边作为上限
int down = best[i - 1][j], up = j == k ? i : best[i][j + 1];
int min =