指尖划过的轨迹,藏着最细腻的答案~
题目:
给你一个 只包含正整数 的 非空 数组 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
分析:
将一个数组分割成两个等和的子集,如果一个数组和为s,则相当于
- 从一个数组中使用某些元素,其元素和为
s/2。 - s必须为偶数。
若s不为偶数,则s/2不是整数,直接放回false。
如果s是偶数,则相当于从物品数组s中挑选一些物品,其总价值为s/2。
动态规划一般分为3步走:
- 确定dp数组含义: 我们定义dp[i][j]为从0到i-1为下标的nums数组中是否正好有和为j的元素。
- 状态转移方程:
对于某个元素
x=nums[i],我们枚举其选或不选:- 选:子问题变为从0到i-1的nums数组中选出一个和恰好等于
j-x的子序列; - 不选:子问题变为从0到i-1的nums数组中选出一个和恰好等于
j的子序列。
- 选:子问题变为从0到i-1的nums数组中选出一个和恰好等于
- 初始化: 对于上面状态转移方程,当i为0时会出现越界,那我们怎样来初始化边界条件呢?我们可以在dp[0]上面增加一行状态,这样dp[i]变为dp[i+1],dp[i-1]变为dp[i],状态转移方程变为: 将dp[0][0]设置为true,作为一个触发条件,其余值初始化为false。
最终dp[n][s/2]即为最终答案
AC代码:
class Solution {
public:
bool canPartition(vector<int>& nums) {
int s = reduce (nums.begin(), nums.end());
if (s % 2) {
return false;
}
int n = nums.size();
s /= 2;
vector f(n + 1, vector<int>(s + 1));
f[0][0] = true;
for (int i = 0; i < n; i++) {
int x = nums[i];
for (int j = 0; j <= s; j++) {
f[i + 1][j] = j >= x && f[i][j - x] || f[i][j];
}
}
return f[n][s];
}
};
空间优化:
上面状态转移方程对于i来说只用到了i-1,因此我们可以使用一个一维数组来记录之前的状态,反复使用这个一维数组来记录:定义dp[j]为是否存在和为j的子序列。
此时上面状态转移方程变为:
最终答案变为:dp[s/2]
具体的,在代码中我们会将s直接除以2来使用;
并且在遍历j时,我们需要倒序遍历,这是因为我们反复使用一维数组dp,如果正序遍历,在读取dp[j - x]时已经覆盖了上一轮的状态。
此外,设前i个值的和为s2,由于前i个值的子序列的元素和不可能比s2大,因此j可以从开始倒着枚举。
并且我们还可以在循环过程中提前判断dp[s]是否为true。
class Solution {
public:
bool canPartition(vector<int>& nums) {
int s = reduce (nums.begin(), nums.end());
if (s % 2) {
return false;
}
s /= 2;
vector<int> f(s + 1);
f[0] = true;
int s2 = 0;
for (int x : nums) {
s2 = min(s2 + x, s);
for (int j = s2; j >= x; j--) {
f[j] |= f[j - x];
}
if (f[s]) {
return true;
}
}
return false;
}
};