[LeetCode] 416、分割等和子集

题目描述

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

示例:

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

输出: true

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

解题思路

看大佬题解,这又是一个01背包的变形。

问题翻译:给定一个只包含正整数的非空数组。是否可以从这个数组中挑选出一些正整数,每个数只能用一次,使得这些数的和等于整个数组元素的和的一半。

  • 动态规划

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

    • 状态转移方程:面对一个新来的数字nums[i],我们有两种选择:选择/不选择

      • 若不选择:如果在 [0, i - 1] 这个子区间内已经有一部分元素,使得它们的和为 j ,那么 dp[i][j] = true
      • 若选择:如果在 [0, i - 1] 这个子区间内就能找到一部分元素,使得它们的和为 j - nums[i],那么 dp[i][j] = true;(这里的前提是nums[i] <= j

      所以最终的状态转移方程为:dp[i][j] = dp[i - 1][j] or dp[i - 1][j - nums[i]], (nums[i] <= j)

    • 初始化:由状态转移方程可知,后面状态的更新需要前一行的状态(二维dp数组),所以我们需要初始化第一行的数据。

  • 动态规划(状态空间压缩):方法和思想基本类似《剑指offer》第47题。上述dp解法状态空间是 O ( n 2 ) O(n^2) O(n2)。因为我们发现“从第 2 行开始,每一行都参考了前一行的当前位置的值,并且还参考了前一行的小于当前位置的值”。所以当我们在处理第i行时,i-2行及更上面的所有格子所记录的状态都没必要保存下来,所以我们可以将状态空间压缩到 O ( n ) O(n) O(n),用一维数组记录状态。

在这里插入图片描述

​ 注:如果我们用一维dp数组的话,必须从后往前填写。

参考代码

回溯(超时)

// 第一反应:回溯,但是妥妥超时
class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int length = nums.size();
        if(length == 0)
            return false;
        
        int target = accumulate(nums.begin(), nums.end(), 0);
        if(target & 1)  // 特判:如果是奇数,就不符合要求
            return false;
        target >>= 1;
        
        sort(nums.begin(), nums.end());
        int tmpSum = 0;
        return dfs(nums, target, tmpSum, 0);
    }
    
    bool dfs(vector<int>& nums, int target, int tmpSum, int index){
        if(index >= nums.size() || tmpSum > target)
            return false;
        if(tmpSum == target)
            return true;
        
        // 每个元素有两种选择:不选/选
        return dfs(nums, target, tmpSum, index + 1) || dfs(nums, target, tmpSum + nums[index], index + 1);
    }
    
};

二维dp(可AC)

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int length = nums.size();
        if(length == 0)
            return false;
        
        int target = accumulate(nums.begin(), nums.end(), 0);
        if(target & 1)  // 特判 2:如果是奇数,就不符合要求
            return false;
        target >>= 1;
        
        // 创建二维状态数组,行:物品索引,列:容量
        int dp[length][target+1];
        memset(dp, 0, sizeof(dp));
        // 先写第 1 行(由状态转移方程可知,后面状态的更新需要前一行的状态(二维dp数组),所以我们需要初始化第一行的数据)
        for(int i = 1; i <= target; i++){
            if(nums[0] == i)
                dp[0][i] = true;
        }
        
        for(int i = 1; i < length; i++){
            for(int j = 1; j <= target; j++){  // j从0开始也可以,在这里没影响
                dp[i][j] = dp[i-1][j];
                if(j-nums[i] >= 0)
                    dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i]];
            }
        }
        
        return dp[length-1][target];
    }
    
};

// 这样也行(状态转移那里,数组的索引稍微变了下)
class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int length = nums.size();
        if(length == 0)
            return false;
        
        int target = accumulate(nums.begin(), nums.end(), 0);
        if(target & 1)  // 特判 2:如果是奇数,就不符合要求
            return false;
        target >>= 1;
        
        // 创建二维状态数组,行:物品索引,列:容量
        vector<vector<bool> > dp(length+1, vector<bool>(target+1, false));
        dp[0][0] = true;  // 重要
        for(int i = 1; i <= length; i++){
            for(int j = 1; j <= target; j++){  // j从0开始也可以,在这里没影响
                dp[i][j] = dp[i-1][j];
                if(j-nums[i-1] >= 0)
                    dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i-1]];
            }
        }
        
        return dp[length][target];
    }
    
};

一维dp(更快更省空间)

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int length = nums.size();
        if(length == 0)
            return false;
        
        int target = accumulate(nums.begin(), nums.end(), 0);
        if(target & 1)  // 特判 2:如果是奇数,就不符合要求
            return false;
        target >>= 1;
        
        // 创建一维状态数组
        int dp[target+1];
        memset(dp, 0, sizeof(dp));
        // 先写第 1 行(由状态转移方程可知,后面状态的更新需要前一行的状态(二维dp数组),所以我们需要初始化第一行的数据)
        for(int i = 1; i <= target; i++){
            if(nums[0] == i){
                dp[i] = true;
                break;  // 可以提前跳出
            }
        }
        
        for(int i = 1; i < length; i++){
            for(int j = target; j >= 1; j--){  // 用一维dp数组的话,必须从后往前填写
                if(j >= nums[i])
                    dp[j] = dp[j] || dp[j - nums[i]];
            }
        }
        
        return dp[target];
    }
    
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值