题目描述
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
注意:
- 每个数组中的元素不会超过 100
- 数组的大小不会超过 200
示例 1:
示例 2:
解题思路:
这道题是0-1背包问题 我用到了动态规划中备忘录的方法 通过记录每种子集的情况来解题(其实就是枚举了所有的情况 不过复杂度没有那么高)
- 首先进行两次特判 即原来数组长度为0,1,2时候的情况。0和1的时候不可能有子集,所以返回false,2的时候只需看两个元素是否相等就可以了
- 两次特判后我们对原数组求和 如果总和为奇数的话也说明不能分成俩相等的子集,返回false
- 接下来是动态规划备忘录的解法
- 首先开辟一个长度为 (原数组求和)/2+1 长度的bool类型vis数组表示访问状态,这个数组用来表示子集的合的可能性。
比如[1, 5, 11, 5] 当子集是[1,5]的时候,vis[6]就为1 记录下了这次子集的和 子集是[1,5,5]的时候 vis[11]就是1 以此类推(你可能还在代码中看到了max_mid max_max 待会就说) - 然后两重循环
第一重遍历原来数组的全部元素,每次遍历都相当于向已经有的子集(也就是vis数组中值为1的地方)中添加了一个元素 第二重遍历vis,从0~max_mid,来遍历所有已经存在的子集。
- 首先开辟一个长度为 (原数组求和)/2+1 长度的bool类型vis数组表示访问状态,这个数组用来表示子集的合的可能性。
max_mid是添加当前元素(nums[i])之前,上一轮循环中子集的最大和,这样就可以保证我不会遍历0~mid的全部元素,减少了遍历次数 而max_mid的求法则是在遍历vis的时候用max_max获得添加当前元素(nums[i-1])时子集的最大值,然后在退出这个vis循环后用max_max更新mid_max的值 这样在添加下一个元素(nums[i])的时候就可以得到上一次子集的最大值了。
两重循环的目的我通过举一个例子来讲 比如对于[1, 5, 11, 5] nums[0]=1 因此最开始的时候是vis[1]=1 然后进入两重循环
第一重循环中 上一次循环的最大值就是nums[0]=1 因此遍历0~1 在vis[1]处发现vis[1]=1 判断1+5不会超过11(也就是原数组和的一半),因此记录vis[1+5]=1 退出循环 更新max_mid = 6
第二重循环 元素为11 遍历vis中0~6 发现在vis[1]和vis[6]处都为1 但1+11>11,6+11>11因此不进行操作 max_mid 不更新
第三重循环 元素为5 遍历vis中0~6 发现在vis[1]和vis[6]处都为1 但1+5=6,6+5=11因此vis[6]=1 vis[11]=1 此时找到了合为11的子集 返回true
如果循环全部结束还没有找到子集 那就不存在了
代码实现:
class Solution {
public:
bool canPartition(vector<int>& nums) {
int n = nums.size();
if (n < 2)
return false;
else if (n == 2)
return nums[0] == nums[1];
int sum = 0;
for (int i = 0; i < n; ++i) {
sum += nums[i];
}
if (sum % 2 == 1) {
return false;
}
int mid = sum / 2;
vector<bool> accessed(mid + 1, false);
for (int i = 0; i < n; ++i) {
if (nums[i] <= mid) {
swap(nums[0], nums[i]);
break;
}
}
accessed[nums[0]] = true;
int max_mid = nums[0], max_max = nums[0];
for (int i = 1; i < n; ++i)
{
for (int j = 0; j <= max_mid; ++j)
{
if (accessed[j] && j + nums[i] <= mid)
{
accessed[j + nums[i]] = true;
if (j + nums[i] > max_max)
max_max = j + nums[i];
}
if (accessed[mid])
return true;
}
max_mid = max_max;
}
return false;
}
};