题目链接
416. Partition Equal Subset Sum
题目描述
Given a non-empty array containing only positive integers, find if the array can be partitioned into two subsets such that the sum of elements in both subsets is equal.
Note:
Each of the array element will not exceed 100.
The array size will not exceed 200.
Example 1:
Input: [1, 5, 11, 5]
Output: true
Explanation: The array can be partitioned as [1, 5, 5] and [11].
Example 2:
Input: [1, 2, 3, 5]
Output: false
Explanation: The array cannot be partitioned into equal sum subsets.
解题思路
该题目的意思就是看数组中的元素能不能平均分为两半,因此我们首先对原数组进行求和,如果结果是奇数,可以直接返回false。如果结果是偶数,则问题转换为在数组中是否能找到和为sum/2的子数组(注意,子数组中原本数组中的一个数不能使用两次)。
方法一:回溯法
回溯法是解决子集合问题的一个比较经典的方法,无论最后要求原集合中的数字只能用一次还是能用多次。因此,此问题也可以用该方法进行解答。
主要注意的问题:
- 下面的算法如果使用从小到大的顺序对原数组进行排序,则会TLE,改为从大到小之后,AC
- 下面的算法在遍历过程中没有跳过重复的元素,因此具有一点的巧合性,也没充分利用数组的排序结果,正规的算法应该是在遍历过程中跳过重复的元素。
class Solution {
public boolean canPartition(int[] nums) {
if(nums == null || nums.length == 0 || nums.length == 1) {
return false;
}
int sum = getSum(nums);
if((sum & 1) != 0) {
return false;
}
int target = sum >> 1;
Integer[] temp = new Integer[nums.length];
for (int i = 0; i < temp.length; i++) {
temp[i] = nums[i];
}
Arrays.sort(temp, new Comparator<Integer>() {
@Override
public int compare(Integer arg0, Integer arg1) {
return arg1.compareTo(arg0);
}
});
return dfs(temp, 0, target, 0);
}
private boolean dfs(Integer[] nums, int begin, int target, int currSum) {
if(currSum == target) {
return true;
}
if(currSum > target) {
return false;
}
for(int i = begin; i < nums.length; i++) {
if(currSum + nums[i] <= target) { //需要去重
if(dfs(nums, i + 1, target, currSum + nums[i])) {
return true;
}
}else {
return false;
}
}
return false;
}
private int getSum(int[] nums) {
int sum = 0;
for(int i = 0; i < nums.length; i++) {
sum += nums[i];
}
return sum;
}
}
方法二:动态规划
思路,我们新建一个二维数组dp[i][j]
,代表前nums
中的前i
个元素中是否能找到和为j
的子集合。
则有:
- 状态转移方程为
dp[i][j] = dp[i-1][j] || dp[i-1][j - nums[i]]
。该方程比较好理解,dp[i-1][j]
表示和为j的所有元素中不包含当前的元素,dp[i-1][j - nums[i]]
表示和为j
的所有元素中包含当前的元素。 dp
数组的第一行元素都为false
,因此0
个元素不可能和为不为0
的数。dp
数组的第一列元素都为true
,因此,任何一个集合都包含空集,而空集中的数组和为0
。
因此,有了下面的代码:
class Solution {
public boolean canPartition(int[] nums) {
if(nums == null || nums.length == 0 || nums.length == 1) {
return false;
}
int sum = getSum(nums);
if((sum & 1) != 0) {
return false;
}
int target = sum >> 1;
boolean[][] dp = new boolean[nums.length + 1][target + 1];
int row = nums.length + 1;
int col = target + 1;
for(int i = 1; i < row; i++) {
dp[i][0] = true;
}
for(int i = 1; i < col; i++) {
dp[0][i] = false;
}
dp[0][0] = true;
for(int i = 1; i < row; i++) {
for(int j = 1; j < col; j++) {
dp[i][j] |= dp[i-1][j];
if(j - nums[i-1] >= 0) {
dp[i][j] |= dp[i-1][j - nums[i-1]] ;
}
}
}
return dp[row -1][col - 1];
}
private int getSum(int[] nums) {
int sum = 0;
for(int i = 0; i < nums.length; i++) {
sum += nums[i];
}
return sum;
}
}
方法三:背包问题
方法二使用的是二维的dp
数组,观察一下,其实可以将其优化为行数为2
的dp
数组,因为每次更新都只用到了当前数组和其前一行的数组。背包问题的解法和方法二很相似,很像是由方法一优化而来的,但是我不懂为啥关键代码中的第二重for循环需要倒着来遍历(我试着正着进行遍历,但是提交后WA)。其实下面的代码我不懂= =。
class Solution {
public boolean canPartition(int[] nums) {
if(nums == null || nums.length == 0 || nums.length == 1) {
return false;
}
int sum = getSum(nums);
if((sum & 1) != 0) {
return false;
}
int target = sum >> 1;
boolean[] dp = new boolean[target + 1];
dp[0] = true;
for(int num : nums) {
for(int i = target; i >= 1; i--) {
if(i - num >= 0) {
if(dp[i - num] == true) {
dp[i] = true;
}
}
}
}
return dp[target];
}
private int getSum(int[] nums) {
int sum = 0;
for(int i = 0; i < nums.length; i++) {
sum += nums[i];
}
return sum;
}
}