动态规划写法一
算法思路
- 能够分割成两个等和子集,数组的总和
sum必为偶数。 - 我们需要判断的是,和为
sum / 2的子集是否存在,而不是两个子集都要找出来。
dp[i][j]的含义:
dp数组的行代表遍历到哪个nums[i],列代表背包[1, sum / 2],1 <= nums[i] <= 100,第0列是凑数的。
dp[i][j]表示nums中第i个数nums[i]能否填满容量为j的背包。- 可以是
nums[i] == j,也可以是记忆中的j - nums[i]的背包是否被填满,是的话j - nums[i]的状态加上nums[i]可以填满j背包。
注意每一次计算dp[i][j]都要把之前的状态dp[i - 1][j]先转移过来(所谓的不选)
for(int i = 1; i < n; i++){
for(int j = 1; j <= target; j++){
dp[i][j] = dp[i - 1][j];
if(nums[i] == j){
dp[i][j] = true;
continue;
}
if(nums[i] < j){
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i]];
}
}
}
- 因为第
10行有if判断,所以第3行的dp[i - 1][j]不能去掉。 - 为了防止
dp[i - 1][j - nums[i]把dp[i][j]变成false,所以第11行的dp[i - 1][j]不能去掉,如下图。
class Solution {
public boolean canPartition(int[] nums) {
// 先判断nums的和是否为偶数
int sum = 0;
for(int num : nums){
sum += num;
}
if((sum & 1) == 1){
return false;
}
int n = nums.length;
int target = sum / 2;
boolean[][] dp = new boolean[n][target + 1];
if(nums[0] <= target){
dp[0][nums[0]] = true;
}
for(int i = 1; i < n; i++){
for(int j = 1; j <= target; j++){
dp[i][j] = dp[i - 1][j];
if(nums[i] == j){
dp[i][j] = true;
continue;
}
if(nums[i] < j){
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i]];
}
}
}
return dp[n - 1][target];
}
}
由右下角往左上方可以看出11是怎么凑数来的路径,11 - 5 = 6, 6 - 5 = 1,
所以11是由1 + 5 + 5得到的。
动态规划写法二
dp数组的构成:
- 与写法一不同的是,写法二比写法一多了一行。
dp[0][0]默认初始化为true,表示物品重量与背包容量相同。第一个0不是代表0号物品,就当他占一行好了;第二个0可以当成j - nums[i] == 0,也就是物品重量nums[i]与背包容量j相同。第一行其余部分不用管,直接从第二行开始遍历。 - 第
1至n行,表示遍历nums第一个到最后一个数,代码中为nums[i - 1]。列就是背包容量。
为什么要多开一行?
- 为了防止背包只有
0的情况,也就是int[][] dp = new int[n][0 + 1]。比如下图中neg就是背包为0。如果在这种情况下使用写法一,会有导致数组越界,dp[0][nums[0]] = 1, 参考下图 494 目标和
- 当然这一题背包不会为
0,因为if((sum & 1) == 1) {return false;},即便sum = 1, target = sum / 2 = 0,程序也会马上返回。
核心代码:
- 可以看出比写法一更简洁。
- 首先直接转移(或者说
num > j,背包能否装满取决于上一行的背包是否装满。) num <= j的情况:- 如果
num == j,那么就取第一列,第一列统统都为true,表示物品重量与背包容量相同。 - 如果
num < j,那么就取决于上一行是否为true。
for (int i = 1; i <= n; i++) {
int num = nums[i - 1];
for (int j = 1; j <= target; j++) {
dp[i][j] = dp[i - 1][j];
if (num <= j) {
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - num];
}
}
}
class Solution {
public boolean canPartition(int[] nums) {
// 先判断nums的和是否为偶数
int sum = 0;
for (int num : nums) {
sum += num;
}
if ((sum & 1) == 1) {
return false;
}
int n = nums.length;
int target = sum / 2;
boolean[][] dp = new boolean[n + 1][target + 1];
dp[0][0] = true;
for (int i = 1; i <= n; i++) {
int num = nums[i - 1];
for (int j = 1; j <= target; j++) {
dp[i][j] = dp[i - 1][j];
if (num <= j) {
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - num];
}
}
}
return dp[n][target];
}
}