给你一个 只包含正整数 的 非空 数组 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
步骤1:理解题目以及01 背包问题
在 01 背包问题中,我们有一系列物品,每个物品有自己的重量和价值,目标是选择一些物品放入背包中,使得背包的总重量不超过一定限制,同时所选物品的总价值最大。
题目要求将数组分割成两个子集,使得两个子集的元素和相等。这实际上就是在数组中寻找一个子集,使得这个子集的元素和等于整个数组元素和的一半。如果能找到这样的子集,那么剩余的元素的和也必然等于整个数组元素和的一半。
步骤2:将问题转化为 01 背包问题
将这道题目与 01 背包问题联系起来,可以将数组元素看作是物品的重量,而数组元素的值看作是物品的价值。我们的目标是将这些物品放入“背包”中,使得背包的总重量等于数组元素和的一半,同时价值最大。如果我们能找到一种放置物品的方式,使得背包中的物品总价值等于数组元素和的一半,那么剩余的物品总价值也必然等于数组元素和的一半,从而满足题目要求。
步骤3:套用0-1背包问题动态规划思路
在 01 背包问题中,动态规划的状态转移方程通常是:
dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i])
其中,dp[i][j]
表示在前 i
个物品中,背包的容量为 j
时的最大价值,weight[i]
表示第 i
个物品的重量,value[i]
表示第 i
个物品的价值。
在本题中,dp[i][j]
:
i
: 表示考虑前 i
个元素(或前 i
个物品,类比于 01 背包问题中的物品)。j
: 表示目标数值,即我们希望在考虑前 i
个元素的情况下,能够凑出的元素和(或背包的容量,类比于 01 背包问题中的背包容量)。因此,dp[i][j]
的含义是:在考虑前 i
个元素的情况下,能否凑出元素和等于 j
。
具体来说,dp[i][j]
可以有两种可能的取值:
i-1
个元素的情况下,凑出元素和等于 j
,那么我们不需要使用第 i
个元素,所以 dp[i][j] = dp[i-1][j]
。i-1
个元素的情况下,凑出元素和等于 j-nums[i]
,那么我们可以使用第 i
个元素,所以 dp[i][j] = dp[i-1][j-nums[i]]
。步骤4:初始化和边界条件
vector<vector<int>> dp(nums.size(),vector<int>(sum/2+1,0));
for(int i = 0; i < sum/2 + 1; i++){
if(nums[0] <= i){
dp[0][i] = nums[0];
}
}
将 dp
数组的第一行中的值初始化为 0,然后对于第一行中的某个位置 dp[0][i]
,如果 nums[0] <= i
,说明第一个元素可以放入背包中凑出和为 i
,所以将其值设为 nums[0]
;第一列因为j
本身代表元素和,因此j = 0时nums[0][j] = 0
.
步骤5:填充动态规划表
我们使用两个循环来遍历动态规划数组 dp
的每个位置。其中 i
表示考虑前 i
个元素,j
表示目标数值。
i
循环:我们从 i = 1
开始遍历,因为 i = 0
的情况已经在初始化部分处理过了,我们从第二个元素开始考虑。j
循环:我们从 j = 1
开始遍历,因为不需要考虑元素和为 0 的情况,从小到大逐步考虑不同的目标数值。在每个位置 (i, j)
,我们需要根据当前的元素值 nums[i]
以及目标数值 j
,判断是否可以在前 i
个元素中凑出元素和为 j
。我们有两种选择:
nums[i]
大于目标数值 j
,那么无法将当前元素放入凑出目标数值,因此 dp[i][j] = dp[i-1][j]
,即继承前一个状态的值。nums[i]
小于等于目标数值 j
,我们有两种选择:
dp[i][j] = dp[i-1][j]
。i-1
个元素中寻找一种组合,使得元素和为 j-nums[i]
,并且将当前元素值 nums[i]
加上。因此,dp[i][j] = max(dp[i-1][j-nums[i]] + nums[i], dp[i-1][j])
,即选择两种选择中的较大值。通过这两种选择,我们可以逐步填充 dp
数组,最终得到在不同元素和的情况下,是否存在一种组合方式,使得元素和接近或等于目标数值。这就是动态规划状态转移的过程,通过不断更新 dp
数组中的值,我们可以在最后的状态中找到问题的解。
for(int i = 1; i < nums.size(); i++){
for(int j = 1; j < sum/2+1; j++){
if(j < nums[i]){
dp[i][j] = dp[i-1][j];
} else {
dp[i][j] = max(dp[i-1][j-nums[i]] + nums[i], dp[i-1][j]);
}
}
}
步骤6:寻找答案
在填充完整个 dp
数组后,我们需要寻找一个子集,使得其元素和等于整个数组元素和的一半。我们可以在 dp[i][sum/2]
处找到这个值,如果等于 sum/2
,则表示存在这样的子集,返回 true
,否则返回 false
。
for(int i = 0; i < nums.size(); i++){
if(dp[i][sum/2] == sum/2){
return true;
}
}
return false;
总代码(和上面有一点点不同,进行了一定的变量改进)
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = 0;
for(int i = 0; i < nums.size(); i++){
sum += nums[i];
}
// 如果数组元素和为奇数,无法分割成两个元素和相等的子集
if(sum % 2 == 1){
return false;
}
int target = sum / 2; // 目标数值为元素和的一半
vector<vector<int>> dp(nums.size(),vector<int>(target+1,0));
// 初始化第一行,表示只考虑第一个元素时的情况
for(int i = 0; i < target + 1; i++){
if(nums[0] <= i){
dp[0][i] = nums[0];
}
}
// 填充动态规划数组
for(int i = 1; i < nums.size(); i++){
for(int j = 1; j < target + 1; j++){
if(j < nums[i]){
// 当前元素值大于目标数值,无法放入背包
dp[i][j] = dp[i-1][j];
}else{
// 选择放入当前元素或不放入,取较大值
dp[i][j] = max(dp[i-1][j-nums[i]]+nums[i], dp[i-1][j]);
}
}
}
// 检查是否有一种组合方式使得元素和等于目标数值
for(int i = 0; i < nums.size(); i++){
if(dp[i][target] == target){
return true;
}
}
return false;
}
};
思路和上面大差不差,但是可以节约空间。
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = 0;
for(int i = 0; i < nums.size(); i++){
sum += nums[i];
}
// 如果数组元素和为奇数,无法分割成两个元素和相等的子集
if(sum % 2 == 1){
return false;
}
int target = sum / 2; // 目标数值为元素和的一半
vector<int> dp(target+1, 0); // 使用一维数组来保存状态
// 填充动态规划数组
for(int i = 0; i < nums.size(); i++){
// 注意循环条件是 j >= nums[i],从后往前遍历
for(int j = target; j >= nums[i]; j--){
// 选择放入当前元素或不放入,取较大值
dp[j] = max(dp[j-nums[i]] + nums[i], dp[j]);
}
}
// 检查是否有一种组合方式使得元素和等于目标数值
if(dp[target] == target){
return true;
}
return false;
}
};