leetcode-master/problems/0416.分割等和子集.md

250 lines
9.0 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<p align="center">
<a href="https://mp.weixin.qq.com/s/RsdcQ9umo09R6cfnwXZlrQ"><img src="https://img.shields.io/badge/PDF下载-代码随想录-blueviolet" alt=""></a>
<a href="https://mp.weixin.qq.com/s/b66DFkOp8OOxdZC_xLZxfw"><img src="https://img.shields.io/badge/刷题-微信群-green" alt=""></a>
<a href="https://space.bilibili.com/525438321"><img src="https://img.shields.io/badge/B站-代码随想录-orange" alt=""></a>
<a href="https://mp.weixin.qq.com/s/QVF6upVMSbgvZy8lHZS3CQ"><img src="https://img.shields.io/badge/知识星球-代码随想录-blue" alt=""></a>
</p>
<p align="center"><strong>欢迎大家<a href="https://mp.weixin.qq.com/s/tqCxrMEU-ajQumL1i8im9A">参与本项目</a>,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!</strong></p>
## 416. 分割等和子集
题目链接https://leetcode-cn.com/problems/partition-equal-subset-sum/
题目难易:中等
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
注意:
每个数组中的元素不会超过 100
数组的大小不会超过 200
示例 1:
输入: [1, 5, 11, 5]
输出: true
解释: 数组可以分割成 [1, 5, 5] 和 [11].
 
示例 2:
输入: [1, 2, 3, 5]
输出: false
解释: 数组不能分割成两个元素和相等的子集.
提示:
* 1 <= nums.length <= 200
* 1 <= nums[i] <= 100
## 思路
这道题目初步看,是如下两题几乎是一样的,大家可以用回溯法,解决如下两题
* 698.划分为k个相等的子集
* 473.火柴拼正方形
这道题目是要找是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
那么只要找到集合里能够出现 sum / 2 的子集总和,就算是可以分割成两个相同元素和子集了。
本题是可以用回溯暴力搜索出所有答案的但最后超时了也不想再优化了放弃回溯直接上01背包吧。
如果对01背包不够了解建议仔细看完如下两篇
* [动态规划关于01背包问题你该了解这些](https://mp.weixin.qq.com/s/FwIiPPmR18_AJO5eiidT6w)
* [动态规划关于01背包问题你该了解这些滚动数组](https://mp.weixin.qq.com/s/M4uHxNVKRKm5HPjkNZBnFA)
## 01背包问题
背包问题大家都知道有N件物品和一个最多能被重量为W 的背包。第i件物品的重量是weight[i]得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
**背包问题有多种背包方式常见的有01背包、完全背包、多重背包、分组背包和混合背包等等。**
要注意题目描述中商品是不是可以重复放入。
**即一个商品如果可以重复多次放入是完全背包而只能放入一次是01背包写法还是不一样的。**
**要明确本题中我们要使用的是01背包因为元素我们只能用一次。**
回归主题:首先,本题要求集合里能否出现总和为 sum / 2 的子集。
那么来一一对应一下本题,看看背包问题如果来解决。
**只有确定了如下四点才能把01背包问题套到本题上来。**
* 背包的体积为sum / 2
* 背包要放入的商品(集合里的元素)重量为 元素的数值,价值也为元素的数值
* 背包如何正好装满,说明找到了总和为 sum / 2 的子集。
* 背包中每一个元素是不可重复放入。
以上分析完我们就可以套用01背包来解决这个问题了。
动规五部曲分析如下:
1. 确定dp数组以及下标的含义
01背包中dp[i] 表示: 容量为j的背包所背的物品价值可以最大为dp[j]。
**套到本题dp[i]表示 背包总容量是i最大可以凑成i的子集总和为dp[i]**
2. 确定递推公式
01背包的递推公式为dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
本题相当于背包里放入数值那么物品i的重量是nums[i]其价值也是nums[i]。
所以递推公式dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
3. dp数组如何初始化
在01背包一维dp如何初始化已经讲过
从dp[j]的定义来看首先dp[0]一定是0。
如果如果题目给的价值都是正整数那么非0下标都初始化为0就可以了如果题目给的价值有负数那么非0下标就要初始化为负无穷。
**这样才能让dp数组在递归公式的过程中取的最大的价值而不是被初始值覆盖了**
本题题目中 只包含正整数的非空数组所以非0下标的元素初始化为0就可以了。
代码如下:
```C++
// 题目中说:每个数组中的元素不会超过 100数组的大小不会超过 200
// 总和不会大于20000背包最大只需要其中一半所以10001大小就可以了
vector<int> dp(10001, 0);
```
4. 确定遍历顺序
在[动态规划关于01背包问题你该了解这些滚动数组](https://mp.weixin.qq.com/s/M4uHxNVKRKm5HPjkNZBnFA)中就已经说明如果使用一维dp数组物品遍历的for循环放在外层遍历背包的for循环放在内层且内层for循环倒叙遍历
代码如下:
```C++
// 开始 01背包
for(int i = 0; i < nums.size(); i++) {
for(int j = target; j >= nums[i]; j--) { // 每一个元素一定是不可重复放入,所以从大到小遍历
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
}
}
```
5. 举例推导dp数组
dp[i]的数值一定是小于等于i的。
**如果dp[i] == i 说明集合中的子集总和正好可以凑成总和i理解这一点很重要。**
用例1输入[1,5,11,5] 为例,如图:
![416.分割等和子集2](https://img-blog.csdnimg.cn/20210110104240545.png)
最后dp[11] == 11说明可以将这个数组分割成两个子集使得两个子集的元素和相等。
综上分析完毕C++代码如下:
```C++
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = 0;
// dp[i]中的i表示背包内总和
// 题目中说:每个数组中的元素不会超过 100数组的大小不会超过 200
// 总和不会大于20000背包最大只需要其中一半所以10001大小就可以了
vector<int> dp(10001, 0);
for (int i = 0; i < nums.size(); i++) {
sum += nums[i];
}
if (sum % 2 == 1) return false;
int target = sum / 2;
// 开始 01背包
for(int i = 0; i < nums.size(); i++) {
for(int j = target; j >= nums[i]; j--) { // 每一个元素一定是不可重复放入,所以从大到小遍历
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
}
}
// 集合中的元素正好可以凑成总和target
if (dp[target] == target) return true;
return false;
}
};
```
* 时间复杂度O(n^2)
* 空间复杂度O(n)虽然dp数组大小为一个常数但是大常数
## 总结
这道题目就是一道01背包应用类的题目需要我们拆解题目然后套入01背包的场景。
01背包相对于本题主要要理解题目中物品是nums[i]重量是nums[i]价值也是nums[i]背包体积是sum/2。
看代码的话就可以发现基本就是按照01背包的写法来的。
## 其他语言版本
Java
```Java
class Solution {
public boolean canPartition(int[] nums) {
int sum = 0;
for (int i : nums) {
sum += i;
}
if ((sum & 1) == 1) {
return false;
}
int length = nums.length;
int target = sum >> 1;
//dp[j]表示前i个元素可以找到相加等于j情况
boolean[] dp = new boolean[target + 1];
//对于第一个元素只有当j=nums[0]时,才恰好填充满
if (nums[0] <= target) {
dp[nums[0]] = true;
}
for (int i = 1; i < length; i++) {
//j由右往左直到nums[i]
for (int j = target; j >= nums[i]; j--) {
//只有两种情况,要么放,要么不放
//取其中的TRUE值
dp[j] = dp[j] || dp[j - nums[i]];
}
//一旦满足,结束,因为只需要找到一组值即可
if (dp[target]) {
return dp[target];
}
}
return dp[target];
}
}
```
Python
```python
class Solution:
def canPartition(self, nums: List[int]) -> bool:
taraget = sum(nums)
if taraget % 2 == 1: return False
taraget //= 2
dp = [0] * 10001
for i in range(len(nums)):
for j in range(taraget, nums[i] - 1, -1):
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])
return taraget == dp[taraget]
```
Go
-----------------------
* 作者微信:[程序员Carl](https://mp.weixin.qq.com/s/b66DFkOp8OOxdZC_xLZxfw)
* B站视频[代码随想录](https://space.bilibili.com/525438321)
* 知识星球:[代码随想录](https://mp.weixin.qq.com/s/QVF6upVMSbgvZy8lHZS3CQ)
<div align="center"><img src=../pics/公众号.png width=450 alt=> </img></div>