主要的思想来源于0/1背包问题,解决方法是动态规划。我们可以想到,把数组分割成两份,并且和相等,那么每一份的和是总和的一半。那么问题就可以转化为找到一组数,使得他们的和逼近sum/2,最后判断最大的和是否等于sum/2,如果是则说明存在这样的组合,也就是存在子集。其和为sum/2,当然了另一个集合的和也就是sum/2。注意到如果数组的总和为奇数,则可以直接判断不存在这样的子集。所以在正式程序开始前可以先判断数组总和是否是奇数。
0/1背包问题的公式为
其中f(n,y)为总价值,wi为货物重量,pi为单个价值的重量,y为货车容量,我们的目标是在不超过货车容量的前提下使得所装的货物总价值最高。
解决此类问题有两种解决方法,一种是直接利用递归方程求解,但是会有重复计算,可以创建一个二维数组记录数值,令其初始值为-1,还可以避免重复计算。另一中方法是迭代程序代替递归,避免了重复计算,速度相对较快,但是所受局限就是权值必须为整数。本题我们分别选择递归程序和迭代程序来做,直接利用上述公式,可得下面代码
方法一:递归程序
class Solution {
public:
bool canPartition(vector<int>& nums) {
if(nums.size()==1){ //不可能分成两个子集
return false;
}
int sum=accumulate(nums.begin(),nums.end(),0);
if(sum%2){
return false;
}
int n=nums.size(),cap=sum/2;
int** cArray=new int*[n];
for(int i=0;i<n;i++){
cArray[i]=new int[cap+1];
for(int j=0;j<cap+1;j++){
cArray[i][j]=-1;
}
}
int re=f(0,cap,nums,cArray);
for(int i=0;i<n;i++){
delete [] cArray[i];
}
delete cArray;
return re==sum/2;
}
int f(int i,int cap,vector<int>& nums,int** cArray)
{
if(cArray[i][cap]>=0) {
return cArray[i][cap];
}
if(i==nums.size()-1){
cArray[i][cap]=cap>=nums[i]?nums[i]:0;
return cArray[i][cap];
}
if(cap<nums[i]){
cArray[i][cap]= cArray[i+1][cap];
}else{
cArray[i][cap]=max(f(i+1,cap,nums,cArray),f(i+1,cap-nums[i],nums,cArray)+nums[i]);
}
return cArray[i][cap];
}
};
方法二 ,迭代程序
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum=accumulate(nums.begin(),nums.end(),0);
if(nums.size()==1||sum%2==1){
return false;
}
int cap=sum/2;
int n=nums.size();
int f[n][cap+1];
int ymax=min(nums[n-1]-1,cap);
//初始化fn,y表示剩余容量,总的容量为sum/2
for(int y=0;y<=ymax;y++)
{
f[n-1][y]=0;
}
for(int y=nums[n-1];y<=cap;y++)
{
f[n-1][y]=nums[n-1];
}
for(int i=n-2;i>0;i--)
{
ymax=min(nums[i]-1,cap);
for(int y=0;y<=ymax;y++)
{// y<Wi 表示容量不够
f[i][y]=f[i+1][y];
}
for(int y=nums[i];y<=cap;y++)
{//y>=wi
f[i][y]=max(f[i+1][y],f[i+1][y-nums[i]]+nums[i]);
}
}
//i=0,上面的迭代计算会这一步计算做准备
if(cap>=nums[0])
{//有机会取到第一个值
f[0][cap]=max(f[1][cap],f[1][cap-nums[0]]+nums[0]);
}else{ //没有机会取到第一个值
f[0][cap]=f[1][cap];
}
return f[0][cap]==cap; //是否装满货车
}
};
方法三,迭代程序的简化版,代码如下,主要是把二维空间压缩到一维
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum =accumulate(nums.begin(),nums.end(),0);
if(nums.size()==1||sum%2==1){
return false;
}
int n= nums.size();
int cap=sum/2;
bool dp[cap+1];
//初始化
for (int i = 1; i <=cap; i++) {
dp[i] =false;
}
dp[0] = true;
for (int i = 0; i<n; i++) {
for (int j =cap; j > 0; j--) {
if (j >= nums[i]) {
dp[j] =dp[j] || dp[j - nums[i]];//要么用第j个数,要么不用
}
}
}
return dp[cap];
}
};
压缩思想来源于下面的代码,dp[i][j]和dp[i-1][j]的关系是层的关系,后者是前者的上一层,所以外层由i控制就够了,主要比较的j这一层的值。为了防止覆盖,要从大的值开始,相当于之前左边是i,右边是i-1,现在变成左边是i,右边是i+1,这和之前的背包一致。
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= sum/2; j++) {
if (j >= nums[i - 1]) {
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]];
}
else {
dp[i][j] = dp[i - 1][j];
}
}
}
其中dp[i][j]表示从第一个元素到第i个元素是否存在能组成和为j的子集,如果可以为true,否则为false。
综合比较,简化版程序代码减少了,但是理解起来有点困难,而且“背包意图”不太明显。第一版程序虽然复杂,但是理解起来容易,而且如果程序要求具体的数组集合,则第一版在计算完f(i,y)后可以直接确定权值(0/1)。