题目描述
给定一个非空的正整数数组 nums ,请判断能否将这些数字分成元素和相等的两部分。
示例 1:
输入:nums = [1,5,11,5]
输出:true
解释:nums 可以分割成 [1, 5, 5] 和 [11] 。
示例 2:
输入:nums = [1,2,3,5]
输出:false
解释:nums 不可以分为和相等的两部分
提示:
1 <= nums.length <= 200 1 <= nums[i] <= 100
注意:本题与主站 416 题相同: https://leetcode.cn/problems/partition-equal-subset-sum/
解法
方法一:动态规划
思考
判断能否分成和相等的两子集。总和为奇数则不可能;否则等价于能否选出和为 \(s/2\) 的子集。
这是 \(01\) 背包的可行问题。\(dp[i][j]\) 表示前 \(i\) 个数能否凑出 \(j\),由「不选」或「选且剩余可凑」转移。
将问题转化为能否选出若干个数使其和为总和的一半。用二维 \(01\) 背包:\(dp[i][j]\) 表示前 \(i\) 个数能否凑出 \(j\)。
时间复杂度 \(O(n \times s)\),空间复杂度 \(O(n \times s)\)。其中 \(s\) 为数组元素和的一半。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 | class Solution:
def canPartition(self, nums: List[int]) -> bool:
s = sum(nums)
if s % 2 != 0:
return False
m, n = len(nums), (s >> 1) + 1
dp = [[False] * n for _ in range(m)]
for i in range(m):
dp[i][0] = True
if nums[0] < n:
dp[0][nums[0]] = True
for i in range(1, m):
for j in range(n):
dp[i][j] = dp[i - 1][j]
if not dp[i][j] and nums[i] <= j:
dp[i][j] = dp[i - 1][j - nums[i]]
return dp[-1][-1]
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 | class Solution {
public boolean canPartition(int[] nums) {
int s = 0;
for (int x : nums) {
s += x;
}
if (s % 2 != 0) {
return false;
}
int m = nums.length, n = (s >> 1) + 1;
boolean[] dp = new boolean[n];
dp[0] = true;
if (nums[0] < n) {
dp[nums[0]] = true;
}
for (int i = 1; i < m; ++i) {
for (int j = n - 1; j >= nums[i]; --j) {
dp[j] = dp[j] || dp[j - nums[i]];
}
}
return dp[n - 1];
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 | class Solution {
public:
bool canPartition(vector<int>& nums) {
int s = 0;
for (int x : nums) s += x;
if (s % 2 != 0) return false;
int m = nums.size(), n = (s >> 1) + 1;
vector<bool> dp(n);
dp[0] = true;
if (nums[0] < n) dp[nums[0]] = true;
for (int i = 1; i < m; ++i) {
for (int j = n - 1; j >= nums[i]; --j) {
dp[j] = dp[j] || dp[j - nums[i]];
}
}
return dp[n - 1];
}
};
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 | func canPartition(nums []int) bool {
s := 0
for _, x := range nums {
s += x
}
if s%2 != 0 {
return false
}
m, n := len(nums), (s>>1)+1
dp := make([]bool, n)
dp[0] = true
if nums[0] < n {
dp[nums[0]] = true
}
for i := 1; i < m; i++ {
for j := n - 1; j >= nums[i]; j-- {
dp[j] = dp[j] || dp[j-nums[i]]
}
}
return dp[n-1]
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | class Solution {
func canPartition(_ nums: [Int]) -> Bool {
let s = nums.reduce(0, +)
if s % 2 != 0 { return false }
let target = s / 2
var dp = Array(repeating: false, count: target + 1)
dp[0] = true
for num in nums {
for j in stride(from: target, through: num, by: -1) {
dp[j] = dp[j] || dp[j - num]
}
}
return dp[target]
}
}
|
方法二:动态规划(空间优化)
思考
方法一的每一行只依赖上一行。容量从大到小更新一维数组,避免同一数被用两次。
每个数最多选一次,内层对容量从大到小更新,空间复杂度降为 \(O(s)\)。
方法三:记忆化搜索
思考
方法二是递推。按「选或不选当前数」记忆化搜索,状态为下标与已选和,复杂度同阶。
枚举每个数选或不选,用记忆化避免重复状态。
时间复杂度 \(O(n \times s)\),空间复杂度 \(O(n \times s)\)。