分割等和子集

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/partition-equal-subset-sum

问题叙述

给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

示例 1:
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。

示例 2:
输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。

提示:
1 <= nums.length <= 200
1 <= nums[i] <= 100

分析

经典的01背包问题,要使得两个子集的元素和相等,那么首先子集的元素和必须为偶数才有可能.
所以我们先求出数组元素的总和,如果是奇数就直接返回false。如果是偶数,那我们就转化为容量大小为sum/2的背包
于是就可以创建一个二维数组dp[nums.length][sum / 2 + 1];
dp[i][j]表示:容量为j的背包,从前i个元素中任取,能获得的最大元素和

每个元素只有两种状态:取与不取。

  • 如果我们不取该元素的话,那么状态转移方程为:dp[i][j] = dp[i-1][j];
  • 如果我们取该元素的话,那我们必须事先留出该元素所需要的的空间,那么就是在背包容量为j-nums[i]的条件下,加上该元素。对应的状态转移方程为:dp[i][j] = dp[i-1][j-nums[i]] + nums[i];

最终我们的dp[i][j]就是在上述两种情况中,选取最大值
所以最终的状态转移方程为:dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - nums[i]] + nums[i]);

绝大多数的二维dp可以优化成一维dp。
注意到状态转移方程只与容量为j-nums[i]的背包有关,放在二维网格里,就是与其左上方的某个格子有关。
所以我们把dp数组降成一维的时候,必须从后往前遍历,这样才能保证dp[j]先比dp[j-nums[i]]变化。

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public boolean canPartition(int[] nums) {
int sum = 0;
for (int n : nums) sum += n;
if (sum % 2 == 1) return false;
int target = sum / 2;
int[][] dp = new int[nums.length][target + 1];
for (int j = 0; j <= target; j++) {
if (j >= nums[0]) dp[0][j] = nums[0];
}
for (int i = 1; i < nums.length; i++) {
for (int j = 1; j <= target; j++) {
if (j >= nums[i]) dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - nums[i]] + nums[i]);
else dp[i][j] = dp[i-1][j];
}
}
return dp[nums.length - 1][target] == target;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public boolean canPartition(int[] nums) {
int sum = 0;
for (int n : nums) sum += n;
if (sum % 2 != 0) return false;
int target = sum / 2;
int[] dp = new int[target + 1];
for (int num : nums)
for (int j = target; j >= 0; j--) {
if(j >= num) dp[j] = Math.max(dp[j], dp[j - num] + num);
}
return dp[target] == target;
}
}

最后一块石头的重量II

问题叙述

分析

跟上题一样,依旧是把石头尽可能平均的分为两堆,这样就能保证剩下的最后一块石头质量最小
还是先求出石头的总质量Sum,把石头平均分为A,B两堆,target = Sum/2,由于整型除法会自动向下取整
所以A堆石头重target,B堆石头重Sum-target,B一定是大于A的,所以B-A=Sum-2*target就是结果
依旧是创建一个二维数组dp[stones.length][target+1];
dp[i][j]表示从前i个元素中任取,能使容量为j的背包,装下的最大质量
每块石头只有两种状态,取与不取

  • 如果不取,那就要保持上一个状态dp[i][j] = dp[i - 1][j];
  • 如果取,就要预留空间,dp[i][j] = dp[i - 1][j-stones[i]] + stones[i];
    最终的dp[i][j]就是从上述两种状态中选取最大情况,dp[i][j] = Math.max(dp[i-1][j], dp[i - 1][j - stones[i]] + stones[i]);

降为一维DP的方法与上题一致

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public int lastStoneWeightII(int[] stones) {
int sum = 0;
for (int stone : stones) sum += stone;
int target = sum / 2;
int[][] dp = new int[stones.length][target + 1];
for (int j = stones[0]; j <= target; j++) {
dp[0][j] = stones[0];
}
for (int i = 1; i < stones.length; i++) {
for (int j = 1; j <= target; j++) {
if (j >= stones[i]) dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - stones[i]] + stones[i]);
else dp[i][j] = dp[i - 1][j];
}
}
return sum - 2 * dp[stones.length - 1][target];
}
}
1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public int lastStoneWeightII(int[] stones) {
int sum = 0;
for (int stone : stones) sum += stone;
int target = sum / 2;
int[] dp = new int[target + 1];
for (int stone : stones)
for (int j = target; j >= 0; j--)
if (j >= stone) dp[j] = Math.max(dp[j], dp[j - stone] + stone);
return sum - 2 * dp[target];
}
}

目标和

问题叙述

分析

Code

这次就不写二维DP了,以后能用一维DP就用一维的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int sum = 0;
for (int num : nums) sum += num;
if ((sum + target) % 2 == 1 || Math.abs(target) > sum) return 0;
int size = (sum + target) / 2;
int[] dp = new int[size + 1];
dp[0] = 1;
for (int num : nums) {
for (int j = size; j >= num; j--) {
dp[j] += dp[j - num];
}
}
return dp[size];
}
}