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