【LeetCode】416. 分割等和子集

416. 分割等和子集(中等)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

方法一: 0-1背包问题的普通解法

  1. 思路

    • 首先,对题目做一个等价转换: 「是否可以从数组中选择一些正整数,使这些数的和等于整个数组元素和的一半」。

    • 这样就可以看作一个 0-1背包问题, 它的特点是:每个数只能用一次。解决的基本思路是:物品一个个选,容量也一点一点增加去考虑

    • 具体的做法是:画一个 n * (mid+1) 的表格,其中 n 是物品的个数, mid 是背包的容量。n 行表示一个个物品考虑, mid+1 多出来的那一列,表示容量从 0 开始考虑。

    • 状态定义dp[i][j] 表示从数组的 [0,i] 这个子区间内挑选一些正整数,每个数只能用一次,使这些数的和恰好等于 j

    • 状态转移方程:很多时候,状态转移方程的思考角度是 分类讨论 ,对于 0-1 背包问题就是 「考虑当前的数字选或者不选」。

      • 不选择 nums[i] ,如果在 [0, i-1] 这个子区间内已经有一部分元素,使得它们的和为 j ,那么 dp[i][j] = true;
      • 选择 nums[i] ,如果在 [0,i] 这个子区间内就得找到一部分元素,使得它们的和为 j-nums[i]。
    • 初始化条件

      • j-nums[i] 作为数组下标,需要保证大于等于 0,因此 nums[i] <= j ;
      • 当 j == nums[i] 的时候,一定能够保证 dp[i][j] = true;
    • 完整的状态转移方程在这里插入图片描述

  2. 代码

    class Solution {
    public:
        bool canPartition(vector<int>& nums) {
            int n = nums.size();
            int sum = accumulate(nums.begin(), nums.end(), 0);
            // 特判:如果sum为奇数 则不符合要求
            if(sum % 2) return false;
            int mid = sum / 2;
            // dp[0][0] = false : 因为数组中不含0 因此元素和不可能为0
            vector<vector<bool>> dp(n, vector<bool>(mid + 1, false));
            // 先填表格的第0行,第一个数只能让容积为它自己的背包装满
            if(nums[0] <= mid){
                dp[0][nums[0]] = true;
            }
            for(int i=1; i<n; ++i){
                for(int j=0; j<=mid; ++j){
                    // 直接把上一行的结果抄下来,之后再修正
                    dp[i][j] = dp[i-1][j];
                    if(j > nums[i]){
                        dp[i][j] = dp[i-1][j - nums[i]] || dp[i-1][j];
                    }
                    else if(j == nums[i]){
                        dp[i][j] = true;
                        continue;
                    }
                }
            }
            return dp[n-1][mid];
        }
    };
    
  3. 收获

    • 这道题看起来不难,但是思考了很久,感觉自己还是没掌握背包问题。

解法二:空间压缩

  1. 思路

    • 「0-1背包问题」常规优化:「状态数组」从二维降到一维,减少空间复杂度。
    • 在「填表格」的时候,当前行总是参考了它上面一行「头顶上」那个位置和「左上角」某个位置的值。因此,可以只开一个一维数组,从后向前依次填表即可。
    • 「从后向前」写的过程中,一旦nums[i] <=j 不满足,可以马上退出当前循环,因为后面的j的值肯定越来越小,没有必要继续做判断,直接进入外层循环的下一层。相当于也是一个剪枝,这一点是「从前向后」填表所不具备的。
  2. 代码

    class Solution {
    public:
        bool canPartition(vector<int>& nums) {
            int n = nums.size();
            int sum = accumulate(nums.begin(), nums.end(), 0);
            // 特判:如果sum为奇数 则不符合要求
            if(sum % 2) return false;
            int mid = sum / 2;
            // dp[0][0] = false : 因为数组中不含0 因此元素和不可能为0
            vector<bool> dp(mid + 1, false);
            // 第一个数只能让容积为它自己的背包装满
            if(nums[0] <= mid){
                dp[nums[0]] = true;
            }
            for(int i=1; i<n; ++i){
                for(int j=mid; j>=nums[i]; --j){
                    if(j == nums[i])    dp[j] = true;
                    else if(j > nums[i]){
                        dp[j] = dp[j - nums[i]] || dp[j];
                    }
                }
            }
            return dp[mid];
        }
    };
    
  3. 收获

    • 在已经搞懂这道题的情况下,空间优化就容易很多。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值