玩转动态规划 lee416分割等和子集 0-1背包

24 篇文章 1 订阅
5 篇文章 0 订阅

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

注意:
每个数组中的元素不会超过 100
数组的大小不会超过 200
示例 1:
输入: [1, 5, 11, 5]
输出: true
解释: 数组可以分割成 [1, 5, 5] 和 [11].
示例 2:
输入: [1, 2, 3, 5]
输出: false
解释: 数组不能分割成两个元素和相等的子集.

官方解答 动态规划 0-1背包问题

这是一个典型的“动态规划”问题,并且它的“原形”是“0-1 背包问题”。使用“动态规划”解决问题的思路是“以空间换时间”,“规划”这个词在英文中就是“填表格”的意思,代码执行的过程,也可以称之为“填表格”。

做这道题需要做这样一个等价转换:是否可以从这个数组中挑选出一些正整数,使得这些数的和等于整个数组元素的和的一半 。前提条件是:数组的和一定得是偶数,即数组的和一定得被 2整除,这一点是特判。

  • 本题与 0-1 背包问题有一个很大的不同,即
    0-1 背包问题选取的物品的容积总量不能超过规定的总量;本题选取的数字之和需要恰恰好等于规定的和的一半。这一点区别,决定了在初始化的时候,所有的值应该初始化为 false。 (《背包九讲》的作者在介绍 0-1 背包问题的时候,有强调过这点区别,我在这里也只是再重复一下。)

作为“0-1 背包问题”,它的特点是:“每个数只能用一次”。思路是:物品一个一个选,容量也逐个放大考虑。我们实际生活中也是这样做的,尝试一个一个把候选物品放入“背包”。

具体做法是:画一个 len 行,target + 1 列的表格。这里 len 是物品的个数,target 是背包的容量。len 行表示一个一个物品考虑,target + 1多出来的那 1 列,表示背包容量从 0 开始,很多时候,我们需要考虑这个容量为 0 的数值。

状态定义:dp[i][j]表示从数组的 [0, i] 这个子区间内挑选一些正整数,每个数只能用一次,使得这些数的和恰好等于 j。
状态转移方程:很多时候,状态转移方程思考的角度是“分类讨论”,对于“0-1 背包问题”而言就是“当前考虑到的数字选与不选”。
1、不选择 nums[i],如果在 [0, i - 1] 这个子区间内已经有一部分元素,使得它们的和为 j ,那么 dp[i][j] = true;

2、选择 nums[i],如果在 [0, i - 1] 这个子区间内就得找到一部分元素,使得它们的和为 j - nums[i]。

状态转移方程是:
dp[i][j] = dp[i - 1][j] or dp[i - 1][j - nums[i]]

一般写出状态转移方程以后,就需要考虑边界条件(一般而言也是初始化条件)。

  1. j - nums[i] 作为数组的下标,一定得保证大于等于 0 ,因此 nums[i] <= j;
  2. 注意到一种非常特殊的情况:j 恰好等于 nums[i],即单独 nums[j] 这个数恰好等于此时“背包的容积” j,这也是符合题意的。

二维数组 dp [ ][ ]

public class Solution {

    public boolean canPartition(int[] nums) {
        int len = nums.length;
        if (len == 0) {
            return false;
        }

        int sum = 0;
        for (int num : nums) {
            sum += num;
        }

        // 特判:如果是奇数,就不符合要求
        if ((sum & 1) == 1) {
            return false;
        }

        int target = sum / 2;

        // 创建二维状态数组,行:物品索引,列:容量(包括 0)
        boolean[][] dp = new boolean[len][target + 1];

        // 先填表格第 0 行,第 1 个数只能让容积为它自己的背包恰好装满
        if (nums[0] <= target) {
            dp[0][nums[0]] = true;
        }

        // 再填表格后面几行
        for (int i = 1; i < len; i++) {
            for (int j = 0; j <= target; j++) {
                // 直接从上一行先把结果抄下来,然后再修正
                dp[i][j] = dp[i - 1][j];

                if (nums[i] == j) {
                    dp[i][j] = true;
                    continue;
                }
                if (nums[i] < j) {
                    dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i]];
                }
            }
        }
        return dp[len - 1][target];
    }
}

一维数组 dp【】

“0-1 背包问题”常规优化:“装填数组”从二维降到一维,减少空间复杂度。

在“填表格”的时候,当前行只参考了上一行的值,因此状态数组可以只设置 2 行,使用“滚动数组”的技巧“填表格”即可;

实际上连“滚动数组”都不必,在“填表格”的时候,当前行总是参考了它上面一行 “头顶上” 那个位置和“左上角”某个位置的值。因此,我们可以只开一个一维数组,从后向前依次填表即可。

这一点第 1 次接触的时候,可能会觉得很奇怪,理解的办法是,就拿题目中的示例,画一个表格,自己模拟一遍程序是如何“填表”的行为,就很清楚为什么状态数组压缩到 1 行的时候,需要“从后前向”填表。

“从后向前” 写的过程中,一旦 nums[i] <= j 不满足,可以马上退出当前循环,因为后面的 j 的值肯定越来越小,没有必要继续做判断,直接进入外层循环的下一层。相当于也是一个剪枝,这一点是“从前向后”填表所不具备的。

class Solution {
    public boolean canPartition(int[] nums) {
        if(nums.length==0) return false;
        int sum=0;
        for(int i=0;i<nums.length;i++){
            sum+=nums[i];
        }


        if(sum%2==1) return false;

        int n=nums.length;
        int c=sum/2;
        boolean[] dp=new boolean[c+1];
        dp[0]=true;

        for(int i=0;i<=c;i++){
            if(nums[0]==c) dp[i]=true;
        }

        for(int i=1;i<n;i++){
            for(int j=c;j>=nums[i];j--){
                dp[j]=dp[j] || dp[j-nums[i]];
            }
        }
        return dp[c];
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值