力扣-416. 分割等和子集

1.题目

题目地址(416. 分割等和子集 - 力扣(LeetCode))

https://leetcode.cn/problems/partition-equal-subset-sum/

题目描述

给你一个 只包含正整数 非空 数组 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

2.题解

由于每个数仅能选择一次,这是一个0/1背包问题,为了保证每个数只被选择一次,必须将选择数的操作放在for循环外层

2.1 二维dp数组

思路

首先我们需要判断出哪些情况不可能储存在等和子集并排除,必要条件如下:
1.数组和为偶数(否则无法等分)
2.数组长度大于2(否则无法分割子集)
3.最大元素必须小于总和的一半(否则剩下的所有元素和一定小于总和的一半,不可能等分)

其次,我们开始设计dp数组:
这里的思维是这样的,我们先思考一下要求什么?
我们将问题转化为了如何求出 子集和数组总和一半子集组合
而对于每一个元素,都有且仅有两种选择:选或者不选入子集

所以便可以转换为一个0-1背包问题:
这里容易思考建立一个二位dp数组 dp[i][j];
1.i表示在[0, i-1]的索引范围中选择元素(可以选或者不选)
2.j表示在[0, i-1]的范围限制下,能否存在子集和为j的组合情况
3.对于每一个dp[i][j], 都可以表示为状态转换表达式 dp[i][j] = dp[i-1][j](不选)| dp[i-1][j-nums[i]](选择)
4.我们需要初始化一下dp数组,dp[i][0] = true;(一个都不选) 与 dp[i][nums[0]] = true;(只选择了第一个) 是必然存在的!!!
5.更新dp数组时,外层1->n-1,表示当前选择/不选的元素索引; 内层从1->target表示能否凑成和为j的子集组合

代码

  • 语言支持:C++

C++ Code:


class Solution {
public:
    bool canPartition(vector<int>& nums) {
        // 首先进行判断,是否可能存在等和子集
        int n = nums.size();
        // 1.数组长度必须大于等于2
        if(n < 2) return false;
        // 2.数组和必须为偶数
        int sum = accumulate(nums.begin(), nums.end(), 0);
        if(sum & 1) return false;
        // 3.最大元素必须小于和的一半
        int maxNum = *max_element(nums.begin(), nums.end());
        if(maxNum > sum / 2) return false;

        // 使用DP解决问题, 目的是找出和为sum/2的子集数列
        int target = sum / 2;
        vector<vector<bool>> dp(n, vector<bool>(target + 1, false));
        // 外层遍历元素——0/1问题,选或者不选
        // 1.初始化dp数组
        for(int i = 0; i < n; i++){
            // 如果要求的值为0,无论是在任何范围[0,i]中取任意个都能满足(都不选)
            dp[i][0] = true; 
        }
        dp[0][nums[0]] = true; // 第一个数如果选择的话(之后从1开始遍历,必须讨论索引0的选与不选)
        // 2.更新dp数组
        for(int i = 1; i < n; i++){
            int num = nums[i];
            // 内层处理dp值的问题(能否在索引[0,i]范围内任选元素达成j的和)
            for(int j = 1; j <= target; j++){
                // 这里需要考虑一下 j - num 是否可能越界的情况
                if(j >= num){
                    // 当前可以通过不选/选操作来到达
                    dp[i][j] = dp[i-1][j] | dp[i-1][j - num]; 
                }else{
                    // 否则只能选择不选,不然会超出范围
                    dp[i][j] = dp[i-1][j];
                }
                
            }
        } 
        return dp[n-1][target];
    }
};

复杂度分析

令 n 为数组长度。

  • 时间复杂度:\(O(n\times target)\)
  • 空间复杂度:\(O(n\times target)\)

2.2 一维dp数组(优化)

思路

我们研究状态转移方程发现:dp[i][j] = dp[i-1][j](不选)| dp[i-1][j-nums[i]](选择)
其中每一次的i都是由上一次的i-1得到的,也就是说我们不需要保存每一次[0,i]的状态组合如何,只需要保存上一次的状态[0,i-1]的可能组合即可.
而这里我们每次更新dp数组后,对于下一次遍历来说,其实就已经保存了上一次的数据,所以实际上我们只需要一个一维dp数组即可!

在下一次遍历中,我们如果直接使用该dp数组要注意一个问题:
dp[j] = dp[j] | dp[j-num] 中如果我们从1->target, 我们优先更新的是较小的dp数组,
在之后中若是遇到dp[j-num]需要利用这个索引位的数组,我们发现他在之前已经被更新过了,不是我们想要的保存的上一次的dp数组
所以这里遍历我们从大到小便可以完美避免这个问题,这里优先更新大的dp数组,小的dp数组依旧是保存的上一次的dp数组值

代码

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        // 首先进行判断,是否可能存在等和子集
        int n = nums.size();
        // 1.数组长度必须大于等于2
        if(n < 2) return false;
        // 2.数组和必须为偶数
        int sum = accumulate(nums.begin(), nums.end(), 0);
        if(sum & 1) return false;
        // 3.最大元素必须小于和的一半
        int maxNum = *max_element(nums.begin(), nums.end());
        if(maxNum > sum / 2) return false;

        // 使用DP解决问题, 目的是找出和为sum/2的子集数列
        int target = sum / 2;
        vector<bool> dp(target + 1, false);
        // 外层遍历元素——0/1问题,选或者不选
        // 1.初始化dp数组
        dp[0] = true;
        dp[nums[0]] = true; 
        
        // 2.更新dp数组
        for(int i = 1; i < n; i++){
            int num = nums[i];
            // 内层处理dp值的问题(能否在索引[0,i]范围内任选元素达成j的和)
            for(int j = target; j >= 1; j--){
                // 这里需要考虑一下 j - num 是否可能越界的情况
                if(j >= num){
                    // 当前可以通过不选/选操作来到达
                    dp[j] = dp[j] | dp[j - num]; 
                }    
            }
        } 
        return dp[target];
    }
};

复杂度分析

令 n 为数组长度。

  • 时间复杂度:\(O(n\times target)\)
  • 空间复杂度:\(O(target)\)
posted @ 2024-05-29 14:45  DawnTraveler  阅读(22)  评论(0编辑  收藏  举报