解法:
主要考察两点:
①两个子集的元素和相等代表什么?
②0-1背包问题和完全背包问题的区别?
回到原题:
解决第一个问题:两个子集的元素和相等 => 任何一个子集的元素和等于整个数组元素和的一半。
所以当元素和非偶数时,断然不存在两个子集元素和相等。
接下来就是如何选择其中元素,以构成其和等于数组元素和的一半?
每个元素只能使用一次,很显然就是0-1背包问题。
时间复杂度:O(N * target) 其中,target为元素和的一半
代码: 细节问题:0-1背包问题得用逆序遍历。正序遍历会导致元素被多次使用,而逆序可以解决这个问题。因为当dp[i]时,dp[i-num] 还未被更新。
举个例子:nums = [2, 3]
,target = 5
①倒序遍历(√):
for (int num : nums) {
for (int j = 5; j >= num; j--) {
dp[j] = dp[j] || dp[j - num];
}
}
当num = 2时:
dp[5] = dp[5] || dp[5-2]; (dp[5-2] = dp[3] 此时会用到dp[3],但是dp[3]还没被更新,所以不会用到num这个数字,这里就是倒序和正序的关键区别)
dp[4] = dp[4] || dp[4-2];
dp[3] = dp[3] || dp[3-2];
dp[2] = dp[2] || dp[2-2]; (正确)
②正序遍历(×):
for (int num : nums) {
for (int j = num; j <= 5; j++) {
dp[j] = dp[j] || dp[j - num];
}
}
当num = 2时:
dp[2] = dp[2] || dp[2-2];
dp[3] = dp[3] || dp[3-2];
dp[4] = dp[4] || dp[4-2]; (此时dp[4-2] = dp[2] 这里dp[4]会再用一次num这个数字,而dp[2]已经用过num了,用到了两个num,就是完全背包,而不是0-1背包了)
dp[5] = dp[5] || dp[5-2];
完整代码:
class Solution {
public boolean canPartition(int[] nums) {
int sum = 0;
int n = nums.length;
for(int i = 0; i < n; i++){
sum += nums[i];
}
if(sum % 2 == 1){
return false;
}
int target = sum / 2;
boolean[] dp = new boolean[target+1];
dp[0] = true;
for(int num : nums){
for(int i = target; i >= 0; i--){
if(i - num >= 0){
dp[i] = dp[i] || dp[i - num];
}
}
}
return dp[target];
}
}
为了将代码更清晰展示,将此部分用if语句来写,当然直接在内循环用 i >= num 也可以。
for(int i = target; i >= 0; i--){
if(i - num >= 0){
dp[i] = dp[i] || dp[i - num];
}
}