LeetCode416. 分割等和子集

416. 分割等和子集

文章目录

    • [416. 分割等和子集](https://leetcode.cn/problems/partition-equal-subset-sum/)
      • 一、题目
      • 二、题解
        • 方法一:0-1背包二维数组
        • 方法二:0-1背包一维数组


一、题目

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

示例 1:

输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。

示例 2:

输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。

提示:

  • 1 <= nums.length <= 200
  • 1 <= nums[i] <= 100

二、题解

方法一:0-1背包二维数组

步骤1:理解题目以及01 背包问题

在 01 背包问题中,我们有一系列物品,每个物品有自己的重量和价值,目标是选择一些物品放入背包中,使得背包的总重量不超过一定限制,同时所选物品的总价值最大。

题目要求将数组分割成两个子集,使得两个子集的元素和相等。这实际上就是在数组中寻找一个子集,使得这个子集的元素和等于整个数组元素和的一半。如果能找到这样的子集,那么剩余的元素的和也必然等于整个数组元素和的一半。

步骤2:将问题转化为 01 背包问题

将这道题目与 01 背包问题联系起来,可以将数组元素看作是物品的重量,而数组元素的值看作是物品的价值。我们的目标是将这些物品放入“背包”中,使得背包的总重量等于数组元素和的一半,同时价值最大。如果我们能找到一种放置物品的方式,使得背包中的物品总价值等于数组元素和的一半,那么剩余的物品总价值也必然等于数组元素和的一半,从而满足题目要求。

步骤3:套用0-1背包问题动态规划思路

在 01 背包问题中,动态规划的状态转移方程通常是:

dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i])

其中,dp[i][j] 表示在前 i 个物品中,背包的容量为 j 时的最大价值,weight[i] 表示第 i 个物品的重量,value[i] 表示第 i 个物品的价值。

在本题中,dp[i][j]

  • i: 表示考虑前 i 个元素(或前 i 个物品,类比于 01 背包问题中的物品)。
  • j: 表示目标数值,即我们希望在考虑前 i 个元素的情况下,能够凑出的元素和(或背包的容量,类比于 01 背包问题中的背包容量)。

因此,dp[i][j] 的含义是:在考虑前 i 个元素的情况下,能否凑出元素和等于 j

具体来说,dp[i][j] 可以有两种可能的取值:

  1. 如果可以在考虑前 i-1 个元素的情况下,凑出元素和等于 j,那么我们不需要使用第 i 个元素,所以 dp[i][j] = dp[i-1][j]
  2. 如果可以在考虑前 i-1 个元素的情况下,凑出元素和等于 j-nums[i],那么我们可以使用第 i 个元素,所以 dp[i][j] = dp[i-1][j-nums[i]]

步骤4:初始化和边界条件

vector<vector<int>> dp(nums.size(),vector<int>(sum/2+1,0));
for(int i = 0; i < sum/2 + 1; i++){
    if(nums[0] <= i){
        dp[0][i] = nums[0];
    } 
}

dp 数组的第一行中的值初始化为 0,然后对于第一行中的某个位置 dp[0][i],如果 nums[0] <= i,说明第一个元素可以放入背包中凑出和为 i,所以将其值设为 nums[0];第一列因为j本身代表元素和,因此j = 0时nums[0][j] = 0.

步骤5:填充动态规划表

我们使用两个循环来遍历动态规划数组 dp 的每个位置。其中 i 表示考虑前 i 个元素,j 表示目标数值。

  • i 循环:我们从 i = 1 开始遍历,因为 i = 0 的情况已经在初始化部分处理过了,我们从第二个元素开始考虑。
  • j 循环:我们从 j = 1 开始遍历,因为不需要考虑元素和为 0 的情况,从小到大逐步考虑不同的目标数值。

在每个位置 (i, j),我们需要根据当前的元素值 nums[i] 以及目标数值 j,判断是否可以在前 i 个元素中凑出元素和为 j。我们有两种选择:

  1. 如果当前元素值 nums[i] 大于目标数值 j,那么无法将当前元素放入凑出目标数值,因此 dp[i][j] = dp[i-1][j],即继承前一个状态的值。
  2. 如果当前元素值 nums[i]小于等于目标数值 j,我们有两种选择:
    • 选择不将当前元素放入,那么 dp[i][j] = dp[i-1][j]
    • 选择将当前元素放入,我们需要在前 i-1 个元素中寻找一种组合,使得元素和为 j-nums[i],并且将当前元素值 nums[i] 加上。因此,dp[i][j] = max(dp[i-1][j-nums[i]] + nums[i], dp[i-1][j]),即选择两种选择中的较大值。

通过这两种选择,我们可以逐步填充 dp 数组,最终得到在不同元素和的情况下,是否存在一种组合方式,使得元素和接近或等于目标数值。这就是动态规划状态转移的过程,通过不断更新 dp 数组中的值,我们可以在最后的状态中找到问题的解。

for(int i = 1; i < nums.size(); i++){
    for(int j = 1; j < sum/2+1; j++){
        if(j < nums[i]){
            dp[i][j] = dp[i-1][j];
        } else {
            dp[i][j] = max(dp[i-1][j-nums[i]] + nums[i], dp[i-1][j]);
        }
    }
}

步骤6:寻找答案

在填充完整个 dp 数组后,我们需要寻找一个子集,使得其元素和等于整个数组元素和的一半。我们可以在 dp[i][sum/2] 处找到这个值,如果等于 sum/2,则表示存在这样的子集,返回 true,否则返回 false

 for(int i = 0; i < nums.size(); i++){
    if(dp[i][sum/2] == sum/2){
        return true;
    }
 }
 return false;

总代码(和上面有一点点不同,进行了一定的变量改进)

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int sum = 0;
        for(int i = 0; i < nums.size(); i++){
            sum += nums[i];
        }
        // 如果数组元素和为奇数,无法分割成两个元素和相等的子集
        if(sum % 2 == 1){
            return false;
        }
        
        int target = sum / 2; // 目标数值为元素和的一半
        vector<vector<int>> dp(nums.size(),vector<int>(target+1,0));
        
        // 初始化第一行,表示只考虑第一个元素时的情况
        for(int i = 0; i < target + 1; i++){
            if(nums[0] <= i){
                dp[0][i] = nums[0];
            } 
        }
        
        // 填充动态规划数组
        for(int i = 1; i < nums.size(); i++){
            for(int j = 1; j < target + 1; j++){
                if(j < nums[i]){
                    // 当前元素值大于目标数值,无法放入背包
                    dp[i][j] = dp[i-1][j];
                }else{
                    // 选择放入当前元素或不放入,取较大值
                    dp[i][j] = max(dp[i-1][j-nums[i]]+nums[i], dp[i-1][j]);
                }
            }
        }
        
        // 检查是否有一种组合方式使得元素和等于目标数值
        for(int i = 0; i < nums.size(); i++){
            if(dp[i][target] == target){
                return true;
            }
        }
        
        return false;
    }
};

方法二:0-1背包一维数组

思路和上面大差不差,但是可以节约空间。

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int sum = 0;
        for(int i = 0; i < nums.size(); i++){
            sum += nums[i];
        }
        // 如果数组元素和为奇数,无法分割成两个元素和相等的子集
        if(sum % 2 == 1){
            return false;
        }
        int target = sum / 2; // 目标数值为元素和的一半
        vector<int> dp(target+1, 0); // 使用一维数组来保存状态
        
        // 填充动态规划数组
        for(int i = 0; i < nums.size(); i++){
            // 注意循环条件是 j >= nums[i],从后往前遍历
            for(int j = target; j >= nums[i]; j--){
                // 选择放入当前元素或不放入,取较大值
                dp[j] = max(dp[j-nums[i]] + nums[i], dp[j]);
            }
        }
        
        // 检查是否有一种组合方式使得元素和等于目标数值
        if(dp[target] == target){
            return true;
        }
        
        return false;
    }
};

你可能感兴趣的:(LeetCode刷题,算法,数据结构,LeetCode,动态规划)