题目:
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
示例 2:
输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。
题解:
(快速对齐快捷键:Ctrl + Alt + L)
01背包问题——能不能装满容量为target的背包
本题要求把数组分成两个等和的子集,相当于找到一个子集,其和为sum/2,这个sum/2就是target
具体步骤如下:
1. 特例:如果sum为奇数,那一定找不到符合要求的子集,返回False。
2. dp[j]含义:有没有和为j的子集,有为True,没有为False。
3. 初始化dp数组:长度为target + 1,用于存储子集的和从0到target是否可能取到的情况。
比如和为0一定可以取到(也就是子集为空),那么dp[0] = True。
4. 接下来开始遍历nums数组,对遍历到的数nums[i]有两种操作,一个是选择这个数,一个是不选择这个数。
-不选择这个数:dp不变
-选择这个数:dp中已为True的情况再加上nums[i]也为True。比如dp[0]已经为True,那么dp[0 + nums[i]]也是True
5. 在做出选择之前,我们先逆序遍历子集的和从nums[i]到target的所有情况,判断当前数加入后,dp数组中哪些和的情况可以从False变成True。
(为什么要逆序,是因为dp后面的和的情况是从前面的情况转移过来的,如果前面的情况因为当前nums[i]的加入变为了True,比如dp[0 + nums[i]]变成了True,那么因为一个数只能用一次,dp[0 + nums[i] + nums[i]]不可以从dp[0 + nums[i]]转移过来。如果非要正序遍历,必须要多一个数组用于存储之前的情况。而逆序遍历可以省掉这个数组)
dp[j] = dp[j] or dp[j - nums[i]]
这行代码的意思是说,如果不选择当前数,那么和为j的情况保持不变,dp[j]仍然是dp[j],原来是True就还是True,原来是False也还是False;
如果选择当前数,那么如果j - nums[i]这种情况是True的话和为j的情况也会是True。比如和为0一定为True,只要 j - nums[i] == 0,那么dp[j]就变成了True。
dp[j]和dp[j-nums[i]]只要有一个为True,dp[j]就变成True,因此用or连接两者。
6. 最后就看dp[-1]是不是True,也就是dp[target]是不是True
import java.util.Arrays;
class Solution {
public boolean canPartition(int[] nums) {
//记住这个函数,创建数组的顺序流
//返回值如果存在,则返回;否则返回orElse里的0
//Arrays.stream(nums).sum();
//Arrays.stream(nums).max().orElse(0);
//Arrays.stream(nums).min().orElse(0);
int sumAll = Arrays.stream(nums).sum();
if (sumAll % 2 == 1) {
return false;
}
//这里的除以2不要忘记,因为题目要求是数组中有数的和是sumAll/2就OK
int target = sumAll / 2;
boolean[] dp = new boolean[target + 1];
dp[0] = true;
// 如果是正序遍历,j++,nums[i]从头开始,则nums[i]会一直影响dp[j]的结果,
// for (int i = 0; i < nums.length; i++) {
// for (int j = nums[i]; j < target; j++) {
// dp[j] = dp[j] || dp[j - nums[i]];
// }
// }
// 如果是逆序遍历,j--,nums[i]从头开始,则nums[i]只会影响一次dp[j]的结果
for (int i = 0; i < nums.length; i++) {
for (int j = target; j >= nums[i]; j--) {
dp[j] = dp[j] || dp[j - nums[i]];
}
}
return dp[target];
return dp[target];
}
}
/**
* @author wyl
*/
public class Main {
public static void main(String[] args) {
Solution s = new Solution();
int[] nums = {1, 5, 11, 5};
boolean res = s.canPartition(nums);
System.out.println(res);
}
}
为什么要用逆序遍历的解释:
解释一:
想了半天终于明白了为什么一维动态规划要逆序了。。。我也不知道自己说的好不好对不对。 dp[j] = dp[j] | dp[j - nums[i]] 实际上是用的 i - 1 层 dp[j] 和 dp[j - nums[i]] 得出的。 因此,如果顺序遍历的话, dp[j - num[i]] 会首先被更新成新的值, 然后再算 dp[j] = dp[j] | dp[j - nums[i]] 就不对了, 所以要逆序遍历。
解释二:
这里可能会有人困惑为什么压缩到一维时,要采用逆序。因为在一维情况下,是根据 dp[j] || dp[j - nums[i]]来推d[j]的值,如不逆序,就无法保证在外循环 i 值保持不变 j 值递增的情况下,dp[j - num[i]]的值不会被当前所放入的nums[i]所修改,当j值未到达临界条件前,会一直被nums[i]影响,也即是可能重复的放入了多次nums[i],为了避免前面对后面产生影响,故用逆序。