【LeetCode-动态规划】分割等和子集
题目描述
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
注意:
- 每个数组中的元素不会超过 100
- 数组的大小不会超过 200
示例:
输入: [1, 5, 11, 5]
输出: true
解释: 数组可以分割成 [1, 5, 5] 和 [11].
题目链接: https://leetcode-cn.com/problems/partition-equal-subset-sum/
思路
该问题是 01 背包问题,所谓 01 背包问题,就是将 n 个物品放入容量为 V 的背包,每个物品仅有一件,且有两个属性:体积和价值,每个物品要么放要么不放,求解将哪些物品放入背包能得到最大价值。
推广到这一题,假设数组中元素的和为 s,则 s 的一半为 target = s / 2,那么问题就变为了我们将数组中的元素放入一个容量为 target 的背包里,数组中的元素代表物品的体积,则我们要判断是否存在一种放法使得背包被恰好放满。
根据 01 背包问题的解法,我们可以将状态定义为 dp[i][j](类型为 bool 型),表示从区间 [0, i] 中挑选一些数,每个数只用一次,这些数的和是否恰好等于 j。那么对于第 i 个数,存在两种情况:选择和不选择:
- 选择第 i 个数,则
dp[i][j] = dp[i-1][j-nums[i]]
,说明如果可以从区间 [0, i-1] 个数中选择若干数,它们的和为 j-nums[i],那么也就存在从 [0, i] 中选择若干数,它们的和为 j; - 不选择第 i 个数,则
dp[i][j] = dp[i-1][j]
,说明如果可以从区间 [0, i-1] 中选择若干数,这若干个数的和为 j,那么可以从区间 [0, i] 选择若干数,使得它们的和为 j(不选第 i 个数即可)。
这两种情况只要有一种成立,就说明从 [0, i] 中选择若干数,这若干数的和为 j 是成立的。也就是 dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i]]
。
代码如下:
class Solution {
public:
bool canPartition(vector<int>& nums) {
if(nums.size()<2) return false;
int s = 0;
for(int i=0; i<nums.size(); i++) s += nums[i];
if(s%2!=0) return false; // 不能平分返回 false
int target = s/2;
vector<vector<bool>> dp(nums.size(), vector<bool>(target+1, false));
if(nums[0]<=target) dp[0][nums[0]] = true;
for(int i=1; i<nums.size(); i++){ // 第一层循环选物品
for(int j=0; j<=target; j++){ // 第二层循环将背包容量从0到target递推
if(nums[i]==j){ // 这个判断不能省
dp[i][j] = true;
continue;
}
if(nums[i]<j){
dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i]];
}
}
}
return dp[nums.size()-1][target];
}
};
空间复杂度优化:
01 背包可以进行空间复杂度优化,也就是将二维的 dp 数组改为一维。这样的话,dp[j] 就代表了 dp[i][j]。状态转移方程 dp[i][j] = dp[i-1][j] || dp[i][j-nums[i]]
就变为了 dp[j] = dp[j] || dp[j-nums[i]]
。代码如下:
class Solution {
public:
bool canPartition(vector<int>& nums) {
if(nums.size()<2) return false;
int s = 0;
for(int i=0; i<nums.size(); i++) s += nums[i];
if(s%2!=0) return false; // 不能平分返回 false
int target = s/2;
vector<int> dp(target+1);
if(nums[0]<=target) dp[nums[0]] = true;
for(int i=1; i<nums.size(); i++){ // 第一层循环选物品
for(int j=target; j>=nums[i]; j--){ // 第二层循环将背包容量从0到target递推
if(dp[target]) return true;
dp[j] = dp[j] || dp[j-nums[i]];
}
}
return dp[target];
}
};
需要注意的是,第二层循环要逆序循环,也就是容量从高到低遍历。