一、题目

二、做题思路
1. 问题转化(前置分析)
要将数组分割成两个和相等的子集,等价于从数组中选出一些数,使它们的和等于总和的一半 。
设总和为 sum,若 sum 为奇数,则不可能平分,直接返回 false。若为偶数,目标值 aim = sum / 2。
于是问题转化为一个01背包可行性问题 :有 n 个物品,每个物品重量(数值)为 nums[i],背包容量为 aim,问能否恰好装满。
2. 状态表示(核心基础,包含推导思路)
如何想到用 dp[i][j] ?
-
我们需要记录处理到第几个数 以及当前已经凑出的和是多少。
-
对于每个数,只有"选"或"不选"两种决策,这符合 01 背包的特征。
-
因此,定义
dp[i][j]表示从前i个数中选取,能否凑出总和恰好为j(true表示可行,false表示不可行)。 -
这里
i范围从 0 到n(0 表示一个数都不选),j范围从 0 到aim。这个状态能覆盖所有前缀和组合,满足无后效性,是解决本问题的出发点。
3. 状态转移方程(关键难点)
对于第 i 个数(下标 i-1,数值 nums[i-1]),我们决定不选 或选:
-
不选 :
dp[i][j]继承前i-1个数的结果,即dp[i-1][j]。 -
选 :前提是
j >= nums[i-1],且前i-1个数能凑出j - nums[i-1],即dp[i-1][j - nums[i-1]]为true。只要两者任一为
true,dp[i][j]即为true。因此转移为:
dp[i][j] = dp[i-1][j] || (j >= nums[i-1] && dp[i-1][j - nums[i-1]])。
4. 初始化(边界防护)
-
当目标
j = 0时,不选任何数 即可凑出0,因此dp[i][0] = true(对所有i成立)。 -
当
i = 0(没有数可选)且j > 0时,无法凑出任何正数,dp[0][j] = false(默认false即可)。代码中通过循环
for (int i=0; i<=n; ++i) dp[i][0] = true;实现第一列的初始化。
5. 填表顺序(递推方向)
dp[i][j] 依赖 dp[i-1][j](上一行同列)和 dp[i-1][j - nums[i-1]](上一行左列),因此必须按行从上到下 (i 从 1 到 n),列从左到右 (j 从 1 到 aim)遍历,确保前置状态已就绪。
6. 返回值(目标映射)
最终判断 dp[n][aim] 是否为 true,即所有数中能否选出若干个数凑成 aim。若为 true,则原数组可等和分割;否则不能。代码直接 返回 dp[n][aim]。
三、代码
cpp
class Solution
{
public:
bool canPartition(vector<int>& nums)
{
// 1. 计算数组总和,若为奇数则不可能平分
int sum = 0;
for (auto x : nums)
sum += x;
if (sum % 2 != 0)
return false;
int target = sum / 2; // 目标子集和
int n = nums.size();
// 2. 创建dp表
// dp[i][j] 表示能否从前 i 个元素中选取若干个数,使其和恰好为 j
vector<vector<bool>> dp(n + 1, vector<bool>(target + 1, false));
// 3. 初始化
// 前 i 个元素中,选出和为 0 的方案总是存在(即什么都不选),所以 dp[i][0] = true
for (int i = 0; i <= n; i++)
{
dp[i][0] = true;
}
// 4. 填表顺序:外层遍历物品(i 从 1 到 n),内层遍历容量(j 从 1 到 target)
// 因为 dp[i][j] 依赖于 dp[i-1][j](不选当前数)和 dp[i-1][j-nums[i-1]](选当前数),
// 所以按行从上到下、列从左到右即可。
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= target; j++)
{
// 5. 状态转移方程:
// 不选第 i 个数:继承上一行的状态 dp[i-1][j]
// 选第 i 个数:前提是 j >= nums[i-1],且 dp[i-1][j-nums[i-1]] 为 true
dp[i][j] = dp[i - 1][j];
if (j >= nums[i - 1])
{
dp[i][j] = dp[i][j] || dp[i - 1][j - nums[i - 1]];
}
}
}
// 6. 返回值:dp[n][target] 表示能否从前 n 个数中选出和为 target 的子集
return dp[n][target];
}
};
四、流程图

五、优化
1. 状态表示(核心基础,包含推导思路)
-
若定义二维
dp[i][j]表示前i个数能否凑出j,观察转移方程发现,dp[i][j]只依赖于上一行dp[i-1][...]的值。因此可以压缩为一维数组,用滚动更新表示当前处理到的物品。 -
最终,我们定义
dp[j]表示考虑已处理完的物品,能否凑出总和恰好为j(true可行,false不可行)。随着物品逐个加入,dp[j]不断更新,遍历结束后即代表所有物品的状态。
2. 状态转移方程(关键难点)
对于当前物品 nums[i-1](第 i 个),我们决定是否将其加入子集:
-
不选 :保持原状,
dp[j]不变(等号右边的dp[j]代表上一轮的状态)。 -
选 :前提是
j >= nums[i-1]且上一轮dp[j - nums[i-1]]为true。两者任一满足,则
dp[j]为true,因此转移为:dp[j] = dp[j] || dp[j - nums[i-1]](j从target逆序到nums[i-1])。
3. 初始化(边界防护)
-
目标
j = 0:不选任何数即可凑出 0,因此dp[0] = true。 -
其余
dp[j](j > 0)初始化为false,表示未处理任何物品时无法凑出正数。一维数组默认全
false,只需设置dp[0] = true即可。
4. 填表顺序(递推方向)
关键:必须逆序遍历容量 j(从 target 到 nums[i-1]) 。
因为 dp[j] 的更新依赖 dp[j - nums[i-1]],若从小到大遍历,dp[j - nums[i-1]] 可能在当前物品 的处理中被提前更新,导致物品被重复使用 (违背 01 背包每个物品只能选一次)。逆序确保 dp[j - nums[i-1]] 仍为上一轮(未处理当前物品) 的状态,与二维的 dp[i-1][j - nums[i-1]] 等价。
外层遍历物品 i 从 1 到 n,内层 j 从 target 递减到 nums[i-1]。
5. 返回值(目标映射)
最终判断 dp[target] 是否为 true,即所有数中能否选出若干个数凑成 target。若为 true,则原数组可等和分割;否则不能。代码直接 返回 dp[target]。
cpp
class Solution
{
public:
bool canPartition(vector<int>& nums)
{
// 1. 计算数组总和,若为奇数则不可能平分
int sum = 0;
for (auto x : nums)
{
sum += x;
}
if (sum % 2 != 0)
{
return false;
}
int target = sum / 2; // 目标子集和(即背包容量)
int n = nums.size();
// 2. 创建dp表(一维滚动数组)
// dp[j] 表示当前已考虑的部分元素中,是否存在一个子集,其和恰好为 j
// 由于使用滚动数组,dp[j] 对应二维 dp[i][j] 的当前行状态(i为外层循环索引)
vector<bool> dp(target + 1, false);
// 3. 初始化
// 和为0总是可达的(不选任何元素),因此 dp[0] = true
// 其余 dp[j] 初始为 false,表示尚未找到和为 j 的子集
dp[0] = true;
// 4. 填表顺序:
// 外层循环遍历每个物品(i 从 1 到 n),
// 内层循环遍历容量 j 从 target 递减到 nums[i-1](倒序)
// 原因:dp[j] 依赖于上一行较小容量 dp[j-nums[i-1]] 的状态,
// 如果正序更新,dp[j-nums[i-1]] 可能已经被当前物品更新过,
// 导致每个物品被重复使用,违背0/1背包的"每个物品最多选一次"原则。
// 倒序则保证在更新 dp[j] 时,用到的 dp[j-nums[i-1]] 仍属于"上一轮"(未处理当前物品)的状态。
for (int i = 1; i <= n; i++)
{
for (int j = target; j >= nums[i - 1]; j--)
{
// 5. 状态转移方程(一维形式):
// 当前容量 j 是否可达,取决于:
// - 不选第 i 个数:dp[j] 保持原值(即上一轮的结果,对应二维的 dp[i-1][j])
// - 选第 i 个数:前提是 j >= nums[i-1],且上一轮容量 j-nums[i-1] 可达(即 dp[j-nums[i-1]] 为 true)
// 如果可达,则当前状态 dp[j] 也变为 true
dp[j] = dp[j] || dp[j - nums[i - 1]];
}
}
// 6. 返回值:dp[target] 表示能否从所有数中选出和为 target 的子集
return dp[target];
}
};