LeetCode_119_PartitionEqualSubsetSum


1. question: 分割等和子集(中等)

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

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

示例 1:

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

示例 2:

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

提示:

1
2
1 <= nums.length <= 200
1 <= nums[i] <= 100

2. answers

这道题要求判断给定的数组,能够划分成两个元素和相等的子集。既然是元素和相等,那么肯定要求总元素和为偶数,如果为奇数,肯定是不行的。

之后,可将问题转换为01背包问题,即确定是偶数之后,将其除以二,得到目标值。此时,将问题转换为在给定的元素中,选取子集,判断总和是否能够刚好达到目标值。

类比01背包问题,物品价值value就是给定的元素值,物品权重weight也是自身元素值,背包容量就是 总和/2。注意,此时如果背包刚好装满,也就意味着所装元素的value和就达到了目标值。(因为value和weight是等值的)

此时,问题就成为了:在给定的元素中,背包是否刚好装满。

类比01背包问题,设置二维数组dp[i][j],表示在当前容量j的情况下,所能达到的不超过容量j的最大值,即如果超过了,显然装不下,也就是最多能够装多少。对于最终结果,即dp[n][目标值],也就是在背包为目标值容量下,在给定的n个元素中,所能够达到的最大重量。如果最大容量刚好等于目标值,即能够取得结果,也就是可以将子集划分成两个目标和相等的子集。

二维数组的尺寸为[n, target+1]。注意初始化元素。

至于递推公式,和前面的01背包一样,对于每个物品,只有取和不取两种状态。

  • 取的话,肯定要保证容量足够,即回退到剩余容量刚好够的情况下;判断取和不取两种情况,取最大值(因为保证了肯定不会超过 target,因为不会超过容量)。
  • 不取的话,只有当无论怎么操作,容量都不够时,也就是当前物品的weight大于容量。

代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public class Solution_0119 {

public boolean canPartition(int[] nums) {

// 判断总和是否为偶数,如果是奇数,肯定不能
int sums = 0;
for(int i: nums) sums += i;
if(sums % 2 != 0) return false;

// 定义总value值。
int contains = sums / 2;

// 其实对于背包尺寸来说,并无限制。但是最多肯定不能多于nums.length的。
// 定义数组,设置每个元素代表,当前个数下,选取当前值的最优值。
int[][] result = new int[nums.length][contains + 1];

// 初始化result[x][0]
for (int i = 0; i < result.length; i++) {
result[i][0] = 0;
}

// 初始化result[0][x],如果nums[0]小于当前尺寸,才放入。
for (int i = 0; i < contains + 1; i++) {
if(nums[0] <= i)
result[0][i] = nums[0];
}

// 动态规划
for (int i = 1; i < nums.length; i++) {

for (int j = 1; j < contains + 1; j++) {

// 如果当前数大于容量了,那么显然,当前数肯定是加入不到背包中的,所以当前值的结果只能是上一个数的结果
if(nums[i] > j) result[i][j] = result[i-1][j];

// 否则,即当前值小于容量,那么此时就比较【加入当前值的结果(注意,一定要保证容量,即回退到保证容量的位置),上一个数的结果】
else result[i][j] = Math.max(result[i-1][j], result[i-1][j-nums[i]] + nums[i]);

// 注意,result[i-1][j]是保证了当前值不大于容量j的。
// 而result[i-1][j-nums[j]]则是回退到了容量为 j-nums[j] 的时候,显然该值也是小于 j-nums[j]的,
// 另外,因为 已经回退到了,即容量少了 nums[j],那么加上他,当前也不会大于 j 的。
}
}

return result[nums.length - 1][contains] == contains;
}

public static void main(String[] args) {
System.out.println();

// int[] nums = {1,5,11,5};
int[] nums = {1,2,3,5};

Solution_0119 s = new Solution_0119();

System.out.println(s.canPartition(nums));
}
}

采用一维动态数组优化后的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public boolean canPartition(int[] nums) {

// 判断是否为偶数,如果为奇数,肯定不能分成两个元素和相同的子集。
int sums = 0;
for(int i: nums) sums += i;
if(sums % 2 != 0) return false;

int target = sums / 2;

// 设置一维动态数组,保存结果,dp[i]表示上一个物品及其之前的物品,在各个背包容量下的最优值(小于等于背包容量)。
int[] dp = new int[target + 1];

// 初始化,第一个元素在各个背包容量下的最大值。
for (int i = 0; i < target + 1; i++) {

// 一个物品的重量weight和价值value均为其自身
if(nums[0] <= i) dp[i] = nums[0];
}

// 动态规划后续的每一个物品
for (int i = 1; i < nums.length; i++) {

// 从后向前遍历,保证每个元素使用到的状态都是前一个元素的状态
for (int j = target; j > 0; j--) {

// 如果当前物品大于背包容量,显然只能存上一个元素,即当前元素的该背包容量下的状态是不变的。
if(nums[i] > j) dp[j] = dp[j];

// 如果可以存,那么就需要判断是否需要存了。
else dp[j] = Math.max(dp[j], dp[j-nums[i]] + nums[i]);
}

}

return dp[target] == target;
}

3. 备注

参考力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台 (leetcode-cn.com)代码随想录 (programmercarl.com)


文章作者: 浮云
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 浮云 !
  目录