Medium | LeetCode 416. 分割等和子集 | 0-1背包问题
416. 分割等和子集
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
注意:
- 每个数组中的元素不会超过 100
- 数组的大小不会超过 200
示例 1:
输入: [1, 5, 11, 5]
输出: true
解释: 数组可以分割成 [1, 5, 5] 和 [11].
示例 2:
输入: [1, 2, 3, 5]
输出: false
解释: 数组不能分割成两个元素和相等的子集.
方法一: 暴力搜索
枚举所有的子集, 判断其子集的所有和是否是target/2。
方法二: 0-1 背包问题
问题可转化为在若干个物品中选出一些物品, 每个物品只能使用一次, 这些物品恰好能够填满容量为sum/2的背包。
经典的0-1背包问题: 0-1背包问题:在M件物品取出若干件放在体积为W的背包里,每件物品只有一件,它们有各自的体积 和价值,问如何选择使得背包能够装下的物品的价值最多。
动态规划的思路:一个一个物品去尝试,一点一点扩大考虑能够容纳的容积的大小,整个过程就像是在填写一张二维表格。
-设置状态:dp[i] [j]表示考虑下标[0, i]这个区间里的所有整数,在它们当中是否能够选出一些数,使得这些数之和恰好为整数j
-状态转移方程:
-
不选择 nums[i]: dp[i] [j] = dp[i - 1] [j];
-
选择 nums [i]:
① nums [i] == j, dp[i] [j] = true;
② nums[i] < j, dp[i] [j] = dp[i - 1] [j - nums [i]];
递归
根据以上的状态转移方程, 可以写出如下的递归的代码
public boolean canPartition(int[] nums) {
int target = 0;
for(int num: nums) {
target += num;
}
if (target % 2 == 0) {
target /= 2;
} else {
return false;
}
return knapsack(nums, target) == 1;
}
private int[][] dp;
public int knapsack(int[] value, int w) {
dp = new int[value.length+1][w+1];
return knapsackDp(value, w, value.length - 1);
}
/**
* @param value 每个物品的价值
* @param w 包的最大承重
* @param index 在前index个物品中选择物品装进背包中
* @return 装载前index个物品进w承重的包的最大价值
*/
private int knapsackDp(int[] value, int w, int index) {
// 递归出口(前0件商品, 也就是第一件商品装进背包承重为w的最大价值)
if (w < 0) {
return -1;
}
if (index == 0) {
return (w == 0) ? 1 : -1;
}
if (dp[index][w] != 0) {
return dp[index][w];
}
return dp[index][w] = Math.max(
// 把第index件物品装进背包, 即等价于于【把前index-1件物品装进重量为w - weight[index]的背包能获得的最大价值 + 当前index的价值】
knapsackDp(value, w - value[index], index - 1),
// 第index件物品不装进背包, 即等价于【把前index-1件物品装进重量为w的背包能获得的最大价值】
knapsackDp(value, w, index - 1)
);
}
迭代
public boolean canPartition(int[] nums) {
int n = nums.length;
if (n < 2) {
return false;
}
int sum = 0, maxNum = 0;
// 先对所有数字求和
for (int num : nums) {
sum += num;
maxNum = Math.max(maxNum, num);
}
// 判断奇偶性, 如果是奇数, 直接返回false
if (sum % 2 != 0) {
return false;
}
// 获取得到target的值
int target = sum / 2;
if (maxNum > target) {
return false;
}
boolean[][] dp = new boolean[n][target + 1];
// 填充第0列的值, 全部填充为true
for (int i = 0; i < n; i++) {
dp[i][0] = true;
}
// 填充第0行值
dp[0][nums[0]] = true;
for (int i = 1; i < n; i++) {
// 从第1行开始从左往右, 从上往下开始遍历
int num = nums[i];
for (int j = 1; j <= target; j++) {
if (j >= num) {
// 当前背包容量 大于 当前的体积,
// 那么可有 不把当前物品装进背包dp[i - 1][j]
// 把当前物品装进背包两种选择
dp[i][j] = dp[i - 1][j] | dp[i - 1][j - num];
} else {
// 当前背包容量小于物品体积, 那么只能选择将当前物品不装进背包
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[n - 1][target];
}
优化空间复杂度
第二层的循环我们需要从大到小计算,因为如果我们从小到大更新dp 值,那么在计算 dp[j] 值的时候,dp[j−nums[i]] 已经是被更新过的状态,不再是上一行的dp 值。
public boolean canPartition(int[] nums) {
int n = nums.length;
if (n < 2) {
return false;
}
int sum = 0, maxNum = 0;
for (int num : nums) {
sum += num;
maxNum = Math.max(maxNum, num);
}
if (sum % 2 != 0) {
return false;
}
int target = sum / 2;
if (maxNum > target) {
return false;
}
boolean[] dp = new boolean[target + 1];
dp[0] = true;
for (int i = 0; i < n; i++) {
int num = nums[i];
for (int j = target; j >= num; --j) {
dp[j] |= dp[j - num];
}
}
return dp[target];
}