首页 > 其他分享 >力扣-416. 分割等和子集

力扣-416. 分割等和子集

时间:2024-05-29 14:56:55浏览次数:21  
标签:target nums int 力扣 416 子集 数组 dp

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.题解

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)\)

标签:target,nums,int,力扣,416,子集,数组,dp
From: https://www.cnblogs.com/trmbh12/p/18220202

相关文章

  • 力扣算法之1050. 合作过至少三次的演员和导演
    题解actor_id和director_id,类似一个坐标,只要出现三次或者三次以上就打印出来我的解SELECTactor_id,director_idFROMActorDirectorGROUPBYactor_id,director_idHAVINGCOUNT(1)>=3我的解注解同时分组,两个出现次数大于等于3的就是符合的,看了下,其他的思路和这个......
  • [博客迁移20190713]题解 P4169 【[Violet]天使玩偶/SJY摆棋子】
    《算法竞赛》书上例题(可惜原书没代码)天使玩偶,一道好题。(书p243)我就来谈谈自己的想法吧!而总有人在这种明明可以离线处理的三维偏序问题上投机取巧。如:KDtree。蒟蒻想说,KDtree在这题复杂度是不对的。虽有剪枝,可是还是有可能遍历整棵树的(期望复杂度不靠谱)对上述看法有争议的,请跳......
  • 力扣:2028. 找出缺失的观测数据
    2028.找出缺失的观测数据现有一份 n+m 次投掷单个 六面 骰子的观测数据,骰子的每个面从 1 到 6 编号。观测数据中缺失了 n 份,你手上只拿到剩余 m 次投掷的数据。幸好你有之前计算过的这 n+m 次投掷数据的 平均值 。给你一个长度为 m 的整数数组 rolls......
  • 力扣刷题记录: 2134. 最少交换次数来组合所有的 1 Ⅱ
        这道题是第275场周赛的Q2,LC竞赛分为1748,主要考察滑动窗口。说实话这道题要想到是滑动窗口就很简单,否则就根本无从下手。方法一.滑动窗口(时间超过62.53%C++用户)        处理环形数组的一个很有效的技巧就是“追加”,把整个nums数组追加到nums数组后面,......
  • leetcode力扣 300. 最长递增子序列
    给你一个整数数组nums,找到其中最长严格递增子序列的长度。子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7]是数组[0,3,1,6,2,2,7]的子序列。示例1:输入:nums=[10,9,2,5,3,7,101,18]输出:4解释:最长递增子序列是[2,3,7,101......
  • 力扣 32. 最长有效括号 python AC
    动态规划classSolution:deflongestValidParentheses(self,s):s=''+ssize=len(s)dp=[0]*sizeforiinrange(2,size):ifs[i]==')':ifs[i-1]=='(':......
  • leetcode力扣 1004. 最大连续1的个数 III
    给定一个二进制数组nums和一个整数k,如果可以翻转最多k个0,则返回数组中连续1的最大个数。示例1:输入:nums=[1,1,1,0,0,0,1,1,1,1,0],k=2输出:6解释:[1,1,1,0,0,1,1,1,1,1,1],翻转两个0后,最长的子数组长度为6。示例2:输入:nums=[0,0,1,1,0,0,1,1,1,0,1,1,0,0,0,1,1......
  • 力扣数组题分享
    文章目录前言二分查找思路二分法第一种写法二分法第二种写法螺旋矩阵II思路结尾前言在学习数据结构的过程中,我通过力扣整理了一些常见的数据结构数组题。这些题目帮助我回顾了学习过程中的关键知识点。二分查找力扣题目704.二分查找给定一个n个元素有序......
  • leetcode力扣 2024. 考试的最大困扰度
    一位老师正在出一场由n道判断题构成的考试,每道题的答案为true(用'T'表示)或者false(用'F'表示)。老师想增加学生对自己做出答案的不确定性,方法是最大化有连续相同结果的题数。(也就是连续出现true或者连续出现false)。给你一个字符串answerKey,其中answerKey[i]是第i......
  • 2831. 找出最长等值子数组力扣解法和辅助图
    题目描述:给你一个下标从0开始的整数数组nums和一个整数k。如果子数组中所有元素都相等,则认为子数组是一个等值子数组。注意,空数组是等值子数组。从nums中删除最多k个元素后,返回可能的最长等值子数组的长度。子数组是数组中一个连续且可能为空的元素序列......