分割等和子集——01背包系列

题意

416 分割等和子集

给你一个 只包含正整数 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

提示:

  • 1 <= nums.length <= 200

  • 1 <= nums[i] <= 100

思路分析

回溯法

分析

按照题目的要求,需要在给定的数组找出两个子集,使得这两个子集各自的和相等。一种方法可使用暴力法解,对给出的数组进行回溯,可以画出回溯的树状图的部分来分析:

递归方法中,若刚好等于原数组元素和一半则返回true,单层递归逻辑中的for循环遍历nums数组,这里还需要使用一个数组记录元素被使用的情况,被 之前的递归使用过的元素则将该元素标记,可以使用boolean数组。

回溯实现

/***
  回溯实现方法
*/
private static void backTrack(int[] nums, boolean[] used, int sum, int arrSum) {
    if (sum == arrSum / 2) {
        res = true;
        return;
    }
    for (int i = 0; i < nums.length; i++) {
        if (!used[i]) {
            used[i] = true;
            backTrack(nums, used, sum + nums[i], arrSum);
            used[i] = false;
        }
    }
}

但使用回溯法会超时。

01背包

引入& dp数组的含义

本题还可以使用01背包来解,并且使用一维数组解决。其中,dp[ j ] 代表从nums[0]~nums[i]中选择元素组成子集使得子集的元素和最大

01背包中的0和1代表 不放 两种选择,并且 每个物品都只有一个 ,每个物品有 重量与价值,其中,重量是与背包相对应的,在理论篇中背包装的物品是有重量限制的,价值则与所求dp数组的含义相关,在理论篇中dp数组的含义即选取物品放入背包中,使得背包所背的物品的价值之和最大,求得的dp数组中的最后一个元素,即下标大小为背包限重的位置的dp数组元素,为所求的答案。

对应到本题中,则为:

  • nums数组中的每个元素不能被重复使用 对应 每个物品都只有一个

这里nums数组的元素即对应着理论中的物品;不能重复使用不代表nums数组不包含重复元素,就和不同的物品但是重量和价值都相同一样,这里的不重复是指数组中每个下标对应的元素是只有一个,同一下标的元素不能被多次使用

  • 整个nums数组元素之和sum的一半: sum / 2 对应 背包的限重

要求的便是 能否找到部分元素和使得该元素和为 sum/2

  • nums数组的元素值 对应 物品的重量与价值

这不奇怪,上面讲了,重量与背包限重相关、价值与所求dp的含义相关。本题要求的是 ,背包限重对应本题的 sum/2这个 ,而dp的含义即将nums数组中的数取出组成子集使得子集的元素和最大,也是数。

  • 求得dp[sum / 2] 若为sum/2则代表找到 对应 求得背包的物品最大值

本题使用01背包求得最大值,到此都和01背包的思路和实现都是吻合的,但题目要求 是否可以找到 因此则需要将数组最后一个元素与nums数组元素和的一半比较,两者相等则表示可以找到

理清楚上面01背包和本题的对应后,使用01背包来完成本题目则不难了,直接把模板套上即可。

递推公式

注意物品的重量和价值对应为nums数组的元素值。

dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);

初始化dp数组

使用一维数组时,可以无需初始化dp数组,默认值为0。

遍历顺序

和一维数组的01背包相同,先遍历物品再遍历背包,且背包从大到小倒叙遍历。

for(int i = 0; i < nums.length; i++) {
    for(int j = sum; j > 0; j--) {
        if(j >= nums[i]) {
            dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
        }
    }
}

举例推导dp数组

例子使用了nums[] = {1, 5, 11, 5}

完整Java代码实现

/***
  01背包解法完整代码
*/
class Solution {
    public boolean canPartition(int[] nums) {
        int sum = 0;
        for(int num : nums) sum += num;
        if(sum % 2 != 0) return false;//sum为奇数则一定无法找到满足条件的两个子集
        sum /= 2;
        int[] dp = new int[sum + 1];
        for(int i = 0; i < nums.length; i++) {
            for(int j = sum; j > 0; j--) {
                if(j >= nums[i]) {
                    dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
                }
            }
        }
        return dp[sum] == sum;
    }
}
/***
  使用回溯法完整代码
*/
public class CutEqualSubUnion {
    private static boolean res = false;
    public static void main(String[] args) {
        int[] nums = {1, 5, 11, 5};
        boolean[] used = {false, false, false, false};
        int arrSum = 0;
        for (int n: nums) arrSum += n;
        if(arrSum % 2 == 0) backTrack(nums, used, 0, arrSum);
        System.out.println(res);
    }

    private static void backTrack(int[] nums, boolean[] used, int sum, int arrSum) {
        if (sum == arrSum / 2) {
            res = true;
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            if (!used[i]) {
                used[i] = true;
                backTrack(nums, used, sum + nums[i], arrSum);
                used[i] = false;
            }
        }
    }
}

《代码随想录》刷题记

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值