[LeetCode] 416. Partition Equal Subset Sum

Given a non-empty array nums containing only positive integers, find if the array can be partitioned into two subsets such that the sum of elements in both subsets is equal.

Example 1:

Input: nums = [1,5,11,5]
Output: true
Explanation: The array can be partitioned as [1, 5, 5] and [11].

Example 2:

Input: nums = [1,2,3,5]
Output: false
Explanation: The array cannot be partitioned into equal sum subsets.

Constraints:

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

分割等和子集。

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

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/partition-equal-subset-sum
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

这是一道非常好的动态规划/0-1背包问题。为什么是0-1背包是因为每个数字只能用一次,只能选择用或不用。

首先我们来理解一下题意,题目给了一个非空的正整数数组(nums[i] > 0),请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。这个问题我们可以把他转化为我们能不能从中挑出一些数字,使得这些数字的和正好为所有数字的和的一半。

这里我们创建一个二维的DP数组,定义是从前 i 个数字内选取若干个正整数,是否存在一种方案使得他们的和等于 j。其他讲解可以直接参见代码注释。

时间O(mn)

空间O(mn)

Java实现

 1 class Solution {
 2     public boolean canPartition(int[] nums) {
 3         int len = nums.length;
 4         int sum = 0;
 5         int max = 0;
 6         for (int num : nums) {
 7             sum += num;
 8             max = Math.max(max, num);
 9         }
10         int target = sum / 2;
11 
12         // corner case
13         if (sum % 2 == 1) {
14             return false;
15         }
16         if (max > target) {
17             return false;
18         }
19 
20         // normal case
21         // dp定义是从数组的前 i 个数内选取若干个正整数,是否存在一种方案使得他们的和等于 j
22         boolean[][] dp = new boolean[len][target + 1];
23         // 因为input都是正数,所以无法使得他们的和等于0
24         dp[0][0] = false;
25         // 任何方案,只要不选,都可以使他们的和为0
26         for (int i = 0; i < len; i++) {
27             dp[i][0] = true;
28         }
29         // 选了第一个数字,和为nums[0]
30         dp[0][nums[0]] = true;
31         for (int i = 1; i < len; i++) {
32             for (int j = 1; j < target + 1; j++) {
33                 // 不考虑当前数字
34                 dp[i][j] = dp[i - 1][j];
35                 if (j >= nums[i]) {
36                     dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i]];
37                 }
38             }
39             // 剪枝
40             if (dp[i][target] == true) {
41                 return true;
42             }
43         }
44         return dp[len - 1][target];
45     }
46 }

 

我们注意到DP数组的更新其实每次都只跟上一行的DP值有关,所以我们这里考虑把二维数组降成一维。注意27行为什么 j 指针是从右往左扫描,因为这样我们就不会覆盖掉之前记录的DP值了。

时间O(mn)

空间O(n)

Java实现

 1 class Solution {
 2     public boolean canPartition(int[] nums) {
 3         int len = nums.length;
 4         int sum = 0;
 5         int max = 0;
 6         for (int num : nums) {
 7             sum += num;
 8             max = Math.max(max, num);
 9         }
10         int target = sum / 2;
11 
12         // corner case
13         if (sum % 2 == 1) {
14             return false;
15         }
16         if (max > target) {
17             return false;
18         }
19 
20         // normal case
21         boolean[] dp = new boolean[target + 1];
22         dp[0] = true;
23         if (nums[0] <= target) {
24             dp[nums[0]] = true;
25         }
26         for (int i = 1; i < nums.length; i++) {
27             for (int j = target; j >= nums[i]; j--) {
28                 if (dp[target]) {
29                     return true;
30                 }
31                 dp[j] = dp[j] || dp[j - nums[i]];
32             }
33         }
34         return dp[target];
35     }
36 }

 

最后我再补充一下这道题的 DP 数组如何填表的问题。我引用了这个帖子里的表。因为每个数字只能用一次,所以这张表是一行一行填写的。表格的第一行 0 - 11 表示的是数组和 sum。表格中第一列 sum == 0 他这里都填了 true(所有红色的true),这里有待商榷但是不影响最终结果。

第一行 nums[0] 那一行,如果选择了 nums[0],那么我们可以构造一个子数组使得他的和 = 1,所以在 [1, 1] 那个位置可以填 true。这一行剩下的部分,因为 1 已经选择过了同时无论选不选1,都无法构造成一个更大的 sum,所以 nums[0] 那一行后面全是 false。

第二行 nums[1] 那一行,1 的位置可以继承上一行来的结果,因为在这里我们可以不选择 nums[1],一样可以构造一个数组和 == 1 的子数组。后面 5 的部分为什么可以填 true 是因为直接加5就可以使 sum = 5,同时 6 也是 true 是因为 dp[i - 1][6 - 5] == dp[i - 1][1] == true,所以这里可以继承过来。

之后的部分都类推,需要结合代码理解填表的过程。

LeetCode 题目总结

posted @ 2022-06-28 14:29  CNoodle  阅读(98)  评论(0编辑  收藏  举报