上篇文章 我们讲了划分累加和最接近的题目,只要求集合的累加和最接近即可,本文我们对题目增加下难度,增加一个限制条件。
划分累加和相近的集合Ⅲ
给定一个正数数组 arr ,把 arr 中所有的数字划分成两个集合。
- 如果 arr 长度为 偶数,两个集合包含的个数 一样多;
- 如果 arr 长度为 奇数,两个集合包含的个数 只相差一个。
并且要求两个集合的 累加和最接近 。返回此时较小集合的累加和。
示例 1:
输入: arr = [100, 1, 2, 3]
输出: 5
解释: 最佳方案是:
- 划分成 [100, 1] 和 [2, 3]。
示例 2:
输入: arr = [1, 3, 2, 5, 6]
输出: 8
解释: 最佳方案是:
- 划分成 [2, 6] = 8 和 [1, 3, 5] = 9。
首先我们依然采用最朴素的 暴力递归 来思考这道题目。
思路
这道题就是典型的 从左到右模型 ,因此,递归就可以按照: 当前来到的位置之前的不用考虑,只考虑此时以及后面的该如何选择 的方式思考。
与上一道题类似,返回 不超过总和的一半 sum/2。
另外,集合个数 的限制可以这样考虑:
- 若长度为偶数,划分的累加和较小的集合个数一定是个数的一半。
- 若长度为奇数,划分的累加和较小的集合个数取 {len/2,len/2+1} 中累加和的最大值(取最大是因为这样才能最接近 sum/2)。
代码
public static int ways(int[] arr) {
if (arr == null || arr.length < 2) {
return 0;
}
int sum = 0;
for (int num : arr) {
sum += num;
}
if ((arr.length % 2) == 0) {
return process(arr, 0, arr.length / 2, sum / 2);
} else {
return Math.max(process(arr, 0, arr.length / 2, sum / 2), process(arr, 0, arr.length / 2 + 1, sum / 2));
}
}
// 还剩下 rest 的大小,从 i 位置之后,选择 nums 个数字,还能够选择哪些数
// 选出来的数放入累加和较小的集合中,返回
public static int process(int[] arr, int i, int nums, int rest) {
if (i == arr.length) {
return nums == 0 ? 0 : -1;
}
// 不选择 i 位置,nums、rest 大小不变
int p1 = process(arr, i + 1, nums, rest);
int p2 = -1;
// 此时若 arr[i] 没超过 rest
if (arr[i] <= rest) {
// 之后返回的较小集合是几
p2 = process(arr, i + 1, nums - 1, rest - arr[i]);
}
// p2 == -1,无可行性方案
if (p2 != -1) {
p2 += arr[i];
}
return Math.max(p1, p2);
}
代码解释
递归中的 base case ,如果当前位置 i == arr.length ,如果也不需要再选择数字了,说明是一种有效方案,返回 0,否则返回 -1。
如果不选择当前下标的数字,直接调用下一个,此时的累加和记为 p1。
如果选择当前下标的数字,就要先保证选择这个数之后不会超过总累加和的一半,再调用下一个位置,同时 nums-1。
如果 p2==-1,说明此时选择 i 位置的数就已经让后面无效了。否则累加上当前位置的值,返回累加和 p2。
要想最接近,就要让小集合越大越好,因此返回 p1, p2 的最大值。
之前的文章已经学习了如何通过画 dp 表将暴力递归修改为动态规划。相信小伙伴已经很熟悉了,这次我们就直接修改出动态规划。
动态规划版
public static int dp(int[] arr) {
if (arr == null || arr.length < 2) {
return 0;
}
int sum = 0;
for (int num : arr) {
sum += num;
}
sum /= 2;
int N = arr.length;
int M = (N + 1) / 2;
int[][][] dp = new int[N + 1][M + 1][sum + 1];
for (int i = 0; i <= N; i++) {
for (int j = 0; j <= M; j++) {
for (int k = 0; k <= sum; k++) {
dp[i][j][k] = -1;
}
}
}
for (int rest = 0; rest <= sum; rest++) {
dp[N][0][rest] = 0;
}
for (int i = N - 1; i >= 0; i--) {
for (int picks = 0; picks <= M; picks++) {
for (int rest = 0; rest <= sum; rest++) {
// 不选择 i 位置,nums、rest 大小不变
// 直接到下一层
int p1 = dp[i + 1][picks][rest];
int p2 = -1;
// 还有下一层,且 arr[i] 没超过 rest
if (picks - 1 >= 0 && arr[i] <= rest) {
p2 = dp[i + 1][picks - 1][rest - arr[i]];
}
// 有效
if (p2 != -1) {
p2 += arr[i];
}
dp[i][picks][rest] = Math.max(p1, p2);
}
}
}
// 偶数时返回一半集合的值
if ((arr.length %2) == 0) {
return dp[0][arr.length / 2][sum];
} else {
// 奇数时,返回 一半 和 一半多一 两个集合中较大的(更接近集合总累加和的一半)
return Math.max(dp[0][arr.length / 2][sum], dp[0][(arr.length / 2) + 1][sum]);
}
}
代码解释
可变的参数有三个:下标 i 、选择个数 nums 和剩余数 rest 。因此,需要设置一个三维的 dp 表数组,由于 i, nums, rest 的取值范围为 0n、0(n+1)/2、0~sum/2,因此数组大小设置为 dp[n + 1][(n+1)/2+1][sum/2 + 1] 。
想象一个 三维 的空间坐标系。
由递归可知,普遍位置当前层 依赖下一层 位置的值,因此可以从下层往上层填写 dp 表。
根据 递归调用 可以讨论出 奇偶情况下 应该返回动态规划哪个位置的值。
前面学习的如何一步步的将暴力递归修改为严格表依赖动态规划的基础要打牢哦!还不会的赶快关注一下回顾前面的几篇文章吧!
~ 点赞 ~ 关注 ~ 星标 ~ 不迷路 ~!!!
「全网同名」关注回复「ACM紫书」获取 ACM 算法书籍~