力扣分割等和子集
题目描述
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
注意:
每个数组中的元素不会超过 100
数组的大小不会超过 200
示例 1:
输入: [1, 5, 11, 5]
输出: true
解释: 数组可以分割成 [1, 5, 5] 和 [11].
示例 2:
输入: [1, 2, 3, 5]
输出: false
解释: 数组不能分割成两个元素和相等的子集.
要点分析
一.误区
此题不能用排序然后基于贪心算法依次将每一个元素添加至当前元素和较小的子集中这种类似的算法,这种算法可以轻松的举出反例
二.动态规划思路
这道题可以换一种表述:给定一个只包含正整数的非空数组nums[],判断是否可以从数组中选出一些数字,使得这些数字的和等于整个数组的元素和的一半。因此这个问题可以转换成「0-1背包问题」。这道题与传统的「0-10−1 背包问题」的区别在于,传统的「0-1背包问题」要求选取的物品的重量之和不能超过背包的总容量,这道题则要求选取的数字的和恰好等于整个数组的元素和的一半。类似于传统的「0-1背包问题」,可以使用动态规划求解。
在使用动态规划求解之前,首先需要进行以下判断。
-
根据数组的长度 n 判断数组是否可以被划分。如果 n<2,则不可能将数组分割成元素和相等的两个子集,因此直接返回 false。
-
计算整个数组的元素和 sum 以及最大元素maxn。如果 sum 是奇数,则不可能将数组分割成元素和相等的两个子集,因此直接返回 false。如果 sum 是偶数,则令 tag=sum/2,需要判断是否可以从数组中选出一些数字,使得这些数字的和等于 tag。如果 maxn>tag,则除了 maxn以外的所有元素之和一定小于 tag,因此不可能将数组分割成元素和相等的两个子集,直接返回 false。
创建二维数组 dp,包含 n 行 tag+1 列,其中 dp [ i ] [ j ]表示从数组的 [ 0 ,i ] 下标范围内选取若干个正整数(可以是 0 个),是否存在一种选取方案使得被选取的正整数的和等于 j。初始时,dp 中的全部元素都是false。
在定义状态之后,需要考虑边界情况。以下两种情况都属于边界情况。
-
如果不选取任何正整数,则被选取的正整数等于 0。因此对于所有 0≤i<n,都有dp[i] [0]=true。
-
当 i==0 时,只有一个正整数 nums[0] 可以被选取,因此dp[0] [nums[0]]=true。
对于i>0 且 j>0 的情况,如何确定 dp[i] [j] 的值?需要分别考虑以下两种情况。
-
如果 j ≥nums[i],则对于当前的数字nums[i],可以选取也可以不选取,两种情况只要有一个为 true,就有 dp[i] [j]=true。
-
如果不选取nums[i],则 dp[i] [j]=dp[i−1] [j];
如果选取 nums[i],则 dp[i] [j]=dp[i-1] [j-nums[i]].
如果 j < nums[i],则在选取的数字的和等于 j 的情况下无法选取当前的数字 nums[i],因此有 dp[i] [j]=dp[i−1] [j]。
最终得到dp[n−1] [tag] 即为答案。
AC代码如下:
class Solution {
public:
bool canPartition(vector<int>& nums) {
int n = nums.size();
if (n < 2) {
return false;
}
int sum = accumulate(nums.begin(), nums.end(), 0);
int maxNum = *max_element(nums.begin(), nums.end());
if (sum & 1) {
return false;
}
int target = sum / 2;
if (maxNum > target) {
return false;
}
vector<vector<int>> dp(n, vector<int>(target + 1, 0));
for (int i = 0; i < n; i++) {
dp[i][0] = true;
}
dp[0][nums[0]] = true;
for (int i = 1; i < n; i++) {
int num = nums[i];
for (int j = 1; j <= target; j++) {
if (j >= num) {
dp[i][j] = dp[i - 1][j] | dp[i - 1][j - num];
} else {
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[n - 1][target];
}
};
上述代码的时间复杂度为O(n*tag),但是可以发现计算dp时每一行的dp都只与上一行的dp值有关,因此只需要一个一维数组即可将空间复杂度降到O(tag),此时的转移方程为dp[j]=dp[j]|dp[j-nums[i]], 但要注意的是第二层的循环需要从大到小计算,因为如果我们从小到大更新dp值,那么在计算dp[j]值的时候, dp[j-nums[i]]已经是被更新过的状态,不再是上一行的dp值.
AC代码如下:
bool canPartition(int* nums, int numsSize)
{
int sum=0,maxn=0,tag=0;
for(int i=0;i<numsSize;i++)
{
sum+=nums[i];
if(nums[i]>maxn) maxn=nums[i];
}
tag=sum/2;//求出这个数组的和的一半,如果能找到一些数的和等于tag则满足题意
if(sum%2==1||maxn>tag)return false;//如果sum为奇数或者数组中的最大数都比tag大的话则不满足题意,直接返回false
bool dp[tag+1];
memset(dp,false,sizeof(dp));
dp[0]=true;//设置边界条件
for(int i=0;i<numsSize;i++)
{
for(int j=tag;j>=nums[i];j--)//注意j>=nums[i]不是>
{
dp[j]=dp[j]|dp[j-nums[i]];//表示如果前i-1个数就可以符合题意则加上这个数后依然能符合题意,如果能使j-nums[i]符合题意的话就能使j符合题意
}
}
return dp[tag];
}
三.dfs思路
先求出该数组的和, 再将其分成两堆, 深度优先搜索遍历数组, 只要将最后一个数加到任意一堆使得符合题意即可,然后再递归调用
AC代码如下:
int cmp(const void *a, const void *b) {
return *(int *)b - *(int *)a;
}
int dfs(int *nums, int pos, int taget, int sum1, int sum2) {
if (sum1 == taget || sum2 == taget) {
return true;
}
if (sum1 > taget || sum2 > taget) {
return false;
}
return dfs(nums, pos + 1, taget, sum1 + nums[pos], sum2) || dfs(nums, pos + 1, taget, sum1, sum2 + nums[pos]);
}
bool canPartition(int* nums, int numsSize){
int sum = 0;
for (int i = 0; i < numsSize; i++) {
sum += nums[i];
}
if (sum % 2 != 0) {
return false;
}
sum /= 2;
qsort(nums, numsSize, sizeof(int), cmp);
return dfs(nums, 0, sum, 0, 0);
}