上篇文章 我们讲了划分整数 N 的题目,要求是后面的数不能小于前一个数。
今天我们继续来学习一道与划分有关的题目。
划分累计和相近的集合Ⅱ
给定一个正数数组 arr ,把 arr 中所有的数字划分成两个集合,并且要求两个集合的累加和最接近。返回此时较小集合的累加和。
示例 1:
输入: arr = [100, 1, 2, 3]
输出: 6
解释: 最佳方案是:
- 划分成 [100] 和 [1, 2, 3]。
首先我们依然采用最朴素的 暴力递归 来思考这道题目。
思路
这道题就是典型的 从左到右模型 ,因此,递归就可以按照: 当前来到的位置之前的不用考虑,只考虑此时以及后面的该如何选择 的方式思考。
要想两个集合累加和最接近,那两个集合一定会向着 总和的一半 靠近。
返回较小累加和,那么一定不会超过 总和的一半 。这就是此题的突破口。
代码
public static int ways(int[] arr) {
if (arr == null || arr.length < 2) {
return 0;
}
int sum = 0;
for (int num : arr) {
sum += num;
}
return process(arr, 0, sum / 2);
}
// 还剩下 rest 的大小,从 i 位置之后还能够选择哪些数
// 选出来的数放入累加和较小的集合中
public static int process(int[] arr, int i, int rest) {
if (i == arr.length) {
return 0;
}
// 不选择 i 位置,rest 大小不变
int p1 = process(arr, i + 1, rest);
int p2 = 0;
// 此时若 arr[i] 没超过 rest,可以加入小集合中
if (arr[i] <= rest) {
// arr[i]加入,继续递归下一个位置
p2 = arr[i] + process(arr, i + 1, rest - arr[i]);
}
return Math.max(p1, p2);
}
代码解释
递归中的 base case ,如果当前位置 i == arr.length ,说明数组中已经没有可选的数字了,返回 0 。
如果不选择当前下标的数字,直接调用下一个,此时的累计和记为 p1。
如果选择当前下标的数字,就要先保证选择这个数之后不会超过总累加和的一半。再调用下一个位置,此时的累计和记为 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[][] dp = new int[N + 1][sum + 1];
for (int i = N - 1; i >= 0; i--) {
for (int rest = 0; rest <= sum; rest++) {
int p1 = dp[i + 1][rest];
int p2 = 0;
if (arr[i] <= rest) {
p2 = arr[i] + dp[i + 1][rest - arr[i]];
}
dp[i][rest] = Math.max(p1, p2);
}
}
return dp[0][sum];
}
代码解释
可变的参数有两个:下标 i 和 剩余数 rest 。因此,需要设置一个二维的 dp 表数组,由于 i, rest 的取值范围为 0 ~ n 和 0 ~ sum/2,因此数组大小设置为 dp[n + 1][sum/2 + 1] 。
由递归可知,普遍位置依赖下一行位置的值,因此可以倒着从下往上,从左到右填写 dp 表。
根据递归调用 process(0, sum/2) 可知最终返回 dp[0][sum] 。
前面学习的如何一步步的将暴力递归修改为严格表依赖动态规划的基础要打牢哦!还不会的赶快关注一下回顾前面的几篇文章吧!
~ 点赞 ~ 关注 ~ 星标 ~ 不迷路 ~!!!
「全网同名」关注回复「ACM紫书」获取 ACM 算法书籍~