Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情。
题目:给定一个只包含正整数的非空数组nums。判断是否可以将此数组分割成两个子集,使得两个子集的元素相等。
解题思路
首先题目我们不能纠结这两个子集是啥,我们只需要判断是否可以分割成功即可。那么思路就来了:
- 两个子集之和元素相等,即两个子集相减等于
0。即非空数组中在每个元素前加+或者-,使得最终的等式之和为0。
上面这个思路和Leetcode 494目标和那题就很像了,只不过此处的目标和为0。那么按照那题的思路,我们直接来个回溯,对所有的可能进行判断,代码如下:
public boolean canPartition(int[] nums) {
return canPartition(nums, 0, 0);
}
public boolean canPartition(int[] nums, int index, int curSum){
if(index==nums.length){
if(curSum==0){
return true;
}
return false;
}
boolean add = canPartition(nums, index+1, curSum+nums[index]);
boolean sub = canPartition(nums, index+1, curSum-nums[index]);
return add || sub;
}
时间复杂度为,最终超出时间限制,但思路肯定是ok的。
思路转换
上述的两个子集,我们假设分别为pA和pB,那么就有:
即找到nums中的一组元素,使得元素总和为,此处要求sum必须是偶数,如果是奇数则必定不可能组成。转换之后的问题即变成01背包问题,即背包总重量为,现要求在nums数组中寻找物品,使得物品总重刚好等于背包重量。则根据背包思路可得:
public boolean canPartition2(int[] nums){
int sum = 0;
for(int i=0;i<nums.length;i++){
sum += nums[i];
}
if(sum % 2 == 1) return false;
int weight = sum / 2;
// dp[i][j] 0-i的物品装入重量j的背包可装方式有多少种
// 如果i物品不装 dp[i][j] = dp[i-1][j]
// 如果i物品装,则剩下i-1件物品要装入重量为j-nums[i]的背包中 dp[i][j] = dp[i-1][j-nums[i]
// 则不超重量的前提下:dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i]
int[][] dp = new int[nums.length+1][weight + 1];
for(int i=0;i<nums.length;i++){
dp[i][0] = 1;
}
for(int i=1;i<=nums.length;i++){
for(int j=1;j<=weight;j++){
if(nums[i-1]>j){
dp[i][j] = dp[i-1][j];
}else {
dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i-1]];
}
}
}
if(dp[nums.length][weight]!=0) return true;
return false;
}
背包空间优化,此时要注意的就是背包的顺序从原来的从小到达变成了从大到小,以此保证每个物品只被放入一次:
public boolean canPartition3(int[] nums){
int sum = 0;
for(int i=0;i<nums.length;i++){
sum += nums[i];
}
if(sum % 2 == 1) return false;
int weight = sum / 2;
// dp[i] 物品装入重量为i的背包有多少种方式。
int[] dp = new int[weight + 1];
dp[0] = 1;
for(int i=0;i<nums.length;i++){
for(int j=weight;j>=nums[i];j--){
dp[j] = dp[j] + dp[j-nums[i]];
}
}
if(dp[weight]!=0) return true;
return false;
}