算法:
可以转换为背包问题:
一个商品如果可以重复多次放入是完全背包,而只能放入一次是01背包,写法还是不一样的。
要明确本题中我们要使用的是01背包,因为元素我们只能用一次。
只有确定了如下四点,才能把01背包问题套到本题上来。
- 背包的体积为sum / 2
- 背包要放入的商品:集合里的元素。重量为 元素的数值,价值也为元素的数值
- 背包如果正好装满,说明找到了总和为 sum / 2 的子集。
- 背包中每一个元素不可重复放入。
动规五步曲:
1.确定dp数组及其下标:
01背包中,dp[j] 表示: 容量为j的背包,所背的物品价值最大可以为dp[j]。
本题:dp[j]表示 背包总容量(所能装的总重量)是j,放进物品后,背的最大重量为dp[j]。
2.确定递推公式:
01背包的递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
本题,相当于背包里放入数值,那么物品i的重量是nums[i],其价值也是nums[i]。
所以递推公式:dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
3.dp初始化:
从dp[j]的定义来看,dp[0]一定是0。
如果题目给的价值都是正整数那么非0下标都初始化为0就可以了,如果题目给的价值有负数,那么非0下标就要初始化为负无穷。
这样才能让dp数组在递推的过程中取得最大的价值,而不是被初始值覆盖了。
4.确定遍历顺序:
01背包如果使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历!
// 开始 01背包
for(int i = 0; i < nums.size(); i++) {
for(int j = target; j >= nums[i]; j--) { // 每一个元素一定是不可重复放入,所以从大到小遍历
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
}
}
5.举例推导dp
dp[j]的数值一定是小于等于j的。
如果dp[j] == j 说明,集合中的子集总和正好可以凑成总和j,理解这一点很重要。
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])
-
- `
dp
` 数组初始化为 `[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
`(长度为 `target + 1
`,即 12)。 - `
i = 0
`,`nums[0] = 1
`:- 内层循环从 `
target
`(11)到 `nums[0]
`(1):- `
j = 11
`,`dp[11] = Math.max(0, dp[11 - 1] + 1) = 1
`; - `
j = 10
`,`dp[10] = Math.max(0, dp[10 - 1] + 1) = 1
`; - `
j = 5
`,`dp[5] = Math.max(0, dp[5 - 1] + 1) = 1
`; - `
j = 1
`,`dp[1] = Math.max(0, dp[1 - 1] + 1) = 1
`;
- `
- 内层循环从 `
- `
i = 1
`,`nums[1] = 5
`:- 内层循环从 `
target
`(11)到 `nums[1]
`(5):- `
j = 11
`,`dp[11] = Math.max(1, dp[11 - 5] + 5) = 6
`; - `
j = 10
`,`dp[10] = Math.max(1, dp[10 - 5] + 5) = 6
`; - `
j = 5
`,`dp[5] = Math.max(1, dp[5 - 5] + 5) = 6
`; - `
j = 1
`,`dp[1] = Math.max(1, dp[1 - 5] + 5) = 5
`;
- `
- 内层循环从 `
- `
i = 2
`,`nums[2] = 11
`:- 内层循环从 `
target
`(11)到 `nums[2]
`(11):- `
j = 11
`,`dp[11] = Math.max(6, dp[11 - 11] + 11) = 11
`; - `
j = 10
`,`dp[10] = Math.max(6, dp[10 - 11] + 11) = 11
`; - `
j = 5
`,`dp[5] = Math.max(6, dp[5 - 11] + 11) = 11
`; - `
j = 1
`,`dp[1] = Math.max(6, dp[1 - 11] + 11) = 11
`;
- `
- 内层循环从 `
- `
i = 3
`,`nums[3] = 5
`:- 内层循环从 `
target
`(11)到 `nums[3]
`(5):- `
j = 11
`,`dp[11] = Math.max(11, dp[11 - 5] + 5) = 11
`; - `
j = 10
`,`dp[10] = Math.max(11, dp[10 - 5] + 5) = 11
`; - `
j = 5
`,`dp[5] = Math.max(11, dp[5 - 5] + 5) = 11
`; - `
j = 1
`,`dp[1] = Math.max(11, dp[1 - 5] + 5) = 11
`;
- `
- 内层循环从 `
- `
由于在 `i = 3
` 时 `dp[target] == target
`,所以最终返回 `true
`。
正确代码:
class Solution {
public boolean canPartition(int[] nums) {
int sum = 0;
//求和
for(int a=0; a<nums.length; a++){
sum += nums[a];
}
//若sum为奇数,无法等分
if (sum % 2 != 0) return false;
//求target
int target = sum/2;
int[] dp = new int[target+1];
//dp初始化,所有值都为0(就算不初始化,java也会自动把int数组初始化为0)
for (int i=0; i<dp.length;i++){
dp[i]=0;
}
for(int i = 0; i<nums.length; i++){
for(int j=target; j>=nums[i]; j--){
dp[j] = Math.max(dp[j], dp[j-nums[i]]+nums[i]);
}
}
//若背包装满了,return true
return dp[target] == target;
}}
注意:
1.使用sum时,要将sum初始化:int sum = 0;
2.求target之前,要考虑sum能不能被等分
3.dp数组的长度为target+1
4.for循环时j>=nums[i]
大于等于!
`j>=nums[i]
` 时,考虑的是包括当前物品的情况,因为想要确保当前物品可以放入背包中。
如果使用 `j>nums[i]
`,那么就会漏掉考虑将当前物品放入背包的情况,因为此时不包括当前物品的情况也会被考虑到。
5.函数的返回值是bool,所以最后return dp[target] == target;
6.如果将数组 `dp
` 的初始化部分注释掉,代码仍然能够正常工作
Java中对于未显式初始化的数组,会自动将数组的元素初始化为默认值。
对于基本数据类型 `int
`,其默认值为0。因此,当创建一个 `int
` 类型的数组时,如果没有显式地对数组进行初始化,Java会自动将数组的元素初始化为0。
时间空间复杂度:
-
时间复杂度:O(n^2)
-
空间复杂度:O(n),虽然dp数组大小为一个常数,但是大常数