【LeetCode-动态规划】分割等和子集

题目描述

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

注意:

  • 每个数组中的元素不会超过 100
  • 数组的大小不会超过 200

示例:

输入: [1, 5, 11, 5]

输出: true

解释: 数组可以分割成 [1, 5, 5] 和 [11].

题目链接: https://leetcode-cn.com/problems/partition-equal-subset-sum/

思路

该问题是 01 背包问题,所谓 01 背包问题,就是将 n 个物品放入容量为 V 的背包,每个物品仅有一件,且有两个属性:体积和价值,每个物品要么放要么不放,求解将哪些物品放入背包能得到最大价值。

推广到这一题,假设数组中元素的和为 s,则 s 的一半为 target = s / 2,那么问题就变为了我们将数组中的元素放入一个容量为 target 的背包里,数组中的元素代表物品的体积,则我们要判断是否存在一种放法使得背包被恰好放满。

根据 01 背包问题的解法,我们可以将状态定义为 dp[i][j](类型为 bool 型),表示从区间 [0, i] 中挑选一些数,每个数只用一次,这些数的和是否恰好等于 j。那么对于第 i 个数,存在两种情况:选择和不选择:

  • 选择第 i 个数,则 dp[i][j] = dp[i-1][j-nums[i]],说明如果可以从区间 [0, i-1] 个数中选择若干数,它们的和为 j-nums[i],那么也就存在从 [0, i] 中选择若干数,它们的和为 j;
  • 不选择第 i 个数,则 dp[i][j] = dp[i-1][j],说明如果可以从区间 [0, i-1] 中选择若干数,这若干个数的和为 j,那么可以从区间 [0, i] 选择若干数,使得它们的和为 j(不选第 i 个数即可)。

这两种情况只要有一种成立,就说明从 [0, i] 中选择若干数,这若干数的和为 j 是成立的。也就是 dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i]]

代码如下:

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        if(nums.size()<2) return false;

        int s = 0;
        for(int i=0; i<nums.size(); i++) s += nums[i];
        if(s%2!=0) return false;  // 不能平分返回 false

        int target = s/2;
        vector<vector<bool>> dp(nums.size(), vector<bool>(target+1, false));
        if(nums[0]<=target) dp[0][nums[0]] = true;

        for(int i=1; i<nums.size(); i++){  // 第一层循环选物品
            for(int j=0; j<=target; j++){  // 第二层循环将背包容量从0到target递推
                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[nums.size()-1][target];
    }
};

空间复杂度优化:
01 背包可以进行空间复杂度优化,也就是将二维的 dp 数组改为一维。这样的话,dp[j] 就代表了 dp[i][j]。状态转移方程 dp[i][j] = dp[i-1][j] || dp[i][j-nums[i]] 就变为了 dp[j] = dp[j] || dp[j-nums[i]]。代码如下:

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        if(nums.size()<2) return false;

        int s = 0;
        for(int i=0; i<nums.size(); i++) s += nums[i];
        if(s%2!=0) return false;  // 不能平分返回 false

        int target = s/2;
        vector<int> dp(target+1);
        if(nums[0]<=target) dp[nums[0]] = true;

        for(int i=1; i<nums.size(); i++){  // 第一层循环选物品
            for(int j=target; j>=nums[i]; j--){  // 第二层循环将背包容量从0到target递推
                if(dp[target]) return true;
                dp[j] = dp[j] || dp[j-nums[i]];
            }
        }
        return dp[target];
    }
};

需要注意的是,第二层循环要逆序循环,也就是容量从高到低遍历。

参考

https://leetcode-cn.com/problems/partition-equal-subset-sum/solution/0-1-bei-bao-wen-ti-xiang-jie-zhen-dui-ben-ti-de-yo/

posted @ 2020-08-27 21:12  Flix  阅读(416)  评论(0编辑  收藏  举报