LeetCode416 分割等和子集

       主要的思想来源于0/1背包问题,解决方法是动态规划。我们可以想到,把数组分割成两份,并且和相等,那么每一份的和是总和的一半。那么问题就可以转化为找到一组数,使得他们的和逼近sum/2,最后判断最大的和是否等于sum/2,如果是则说明存在这样的组合,也就是存在子集。其和为sum/2,当然了另一个集合的和也就是sum/2。注意到如果数组的总和为奇数,则可以直接判断不存在这样的子集。所以在正式程序开始前可以先判断数组总和是否是奇数。

      0/1背包问题的公式为               f(n,y)=\begin{cases} p_{n} & \text{if } y\geqslant w_{n} \\ 0& \text{ if } y<w_{n} \end{cases}

                                                           f(i,y)=\begin{cases} max(f(i+1,y),f(i+1,y-w_{i})+p_{i}) & \text{ if } y\geqslant w_{i} \\ f(i+1,y) & \text{ if } 0\leqslant y< w_{i} \end{cases}

其中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)。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
题目描述: 给定一个只包含正整数的非空数组,是否可以将这个数组分成两个子集,使得两个子集的元素和相等。 示例: 输入:[1, 5, 11, 5] 输出:true 解释:数组可以分割成 [1, 5, 5] 和 [11]。 解题思路: 这是一道经典的 0-1 背包问题,可以使用动态规划或者回溯算法解决。 使用回溯算法,需要定义一个 backtrack 函数,该函数有三个参数: - 数组 nums; - 当前处理到的数组下标 index; - 当前已经选择的元素和 leftSum。 回溯过程中,如果 leftSum 等于数组元素和的一半,那么就可以直接返回 true。如果 leftSum 大于数组元素和的一半,那么就可以直接返回 false。如果 index 到达数组末尾,那么就可以直接返回 false。 否则,就对于当前元素,有选择和不选择两种情况。如果选择当前元素,那么 leftSum 就加上当前元素的值,index 就加 1。如果不选择当前元素,那么 leftSum 不变,index 也加 1。最终返回所有可能性的结果中是否有 true。 Java 代码实现: class Solution { public boolean canPartition(int[] nums) { int sum = 0; for (int num : nums) { sum += num; } if (sum % 2 != 0) { return false; } Arrays.sort(nums); return backtrack(nums, nums.length - 1, sum / 2); } private boolean backtrack(int[] nums, int index, int leftSum) { if (leftSum == 0) { return true; } if (leftSum < 0 || index < 0 || leftSum < nums[index]) { return false; } return backtrack(nums, index - 1, leftSum - nums[index]) || backtrack(nums, index - 1, leftSum); } }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值