- 分割等和子集
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
注意:
每个数组中的元素不会超过 100
数组的大小不会超过 200
示例 1:
输入: [1, 5, 11, 5]
输出: true
解释: 数组可以分割成 [1, 5, 5] 和 [11].
示例 2:
输入: [1, 2, 3, 5]
输出: false
解释: 数组不能分割成两个元素和相等的子集.
== 以下全是0-1背包问题的解答模式,从二维降到一维==
要明白为什么能够照抄下来,
dp[i][j] = dp[i - 1][j],其实表达的意思并不是恰好满足,从0-i的所有的数都被使用,而是0-i中某些数字的和满足该条件,导致dp[i]【j】也是成立的
表示第一种情况,不选择当前元素dp[i][j] = dp[i - 1][j]
若用一维数组来表示,即为 dp[i] = dp[i]
class Solution {
public boolean canPartition(int[] nums) {
int len = nums.length;
if(len == 0) return false;
int sum = 0;
for(int num : nums){
sum += num;
}
if(sum % 2== 1) return false;
int target = sum /2;
boolean[][] dp = new boolean[len][target + 1];
if(nums[0] <= target) dp[0][nums[0]] = true;
for(int i= 1; i < len; i++){
for(int j = 0; j <= target; j++){
dp[i][j] = dp[i - 1][j];//先照抄下来,这里要明白深层次原因
if(nums[i] == j){
dp[i][j] = true;
continue;
}else if(nums[i] < j){
dp[i][j] = dp[i - 1][j - nums[i]] || dp[i - 1][j];//注意两种情况
}
}
}
return dp[len - 1][target];
}
}
上面的判断分支过多,可以合并,这个时候需要将dp[0][0] = true,可以参考
class Solution {
public boolean canPartition(int[] nums) {
int len = nums.length;
if(len == 0) return false;
int sum = 0;
for(int num : nums){
sum += num;
}
if(sum % 2== 1) return false;
int target = sum /2;
boolean[][] dp = new boolean[len][target + 1];
dp[0][0] = true;//这里先定义,下面就可以合并分支
if(nums[0] <= target) dp[0][nums[0]] = true;//先初始化第一行
for(int i= 1; i < len; i++){//从1开始,不是从0开始
for(int j = 0; j <= target; j++){
dp[i][j] = dp[i - 1][j];//先照抄下来
if(nums[i] <= j){//这里合并
dp[i][j] = dp[i - 1][j - nums[i]] || dp[i - 1][j];//注意两种情况
}
}
if(dp[i][target]) return true;//这个剪枝,可以提前结束,只要存在一个就一定满足
}
return dp[len - 1][target];
}
}
压缩至一维
0-1 背包问题”常规优化:“状态数组”从二维降到一维,减少空间复杂度。
在“填表格”的时候,当前行只参考了上一行的值,因此状态数组可以只设置 2行
class Solution {
public boolean canPartition(int[] nums) {
int len = nums.length;
if(len == 0) return false;
int sum = 0;
for(int num : nums){
sum += num;
}
if(sum % 2== 1) return false;
int target = sum /2;
boolean[] dp = new boolean[target + 1];//压缩状态,实际移动的就是j
//因为每一行在更新的时候只是用到其上一行的数据,但是要保证在更新的时候上一行的数据不能被覆盖
if(nums[0] <= target) dp[nums[0]] = true;//这里一定要判断,有可能样例是超过的,结果就抛异常
dp[0] = true;
for(int i= 1; i < len; i++){
for(int j = target; j >= 0; j--){
if(j >= nums[i]){
dp[j] = dp[j] || dp[j - nums[i]];
}
}
if(dp[target]) return true;//同时还是要剪枝
}
return dp[target];
}
}
再进一步优化
class Solution {
public boolean canPartition(int[] nums) {
int len = nums.length;
if(len == 0) return false;
int sum = 0;
for(int num : nums){
sum += num;
}
if(sum % 2== 1) return false;
int target = sum /2;
boolean[] dp = new boolean[target + 1];//压缩状态,实际移动的就是j
//因为每一行在更新的时候只是用到其上一行的数据,但是要保证在更新的时候上一行的数据不能被覆盖
if(nums[0] <= target) dp[nums[0]] = true;//不要这句也可以,下面i从0开始,只要把dp[0]= true即可
dp[0] = true;
for(int i= 1; i < len; i++){
for(int j = target; j >= nums[i]; j--){//这里范围发生了变化
if(dp[target]) return true;//同时还是要剪枝,剪枝更彻底
dp[j] = dp[j] || dp[j - nums[i]];
}
}
return dp[target];
}
}