"你的背包,不再让我走得好缓慢"
494. 目标和 : 给你一个整数数组 和一个整数 ,向数组中的每个整数前添加 "" 或 "",然后串联起所有整数,可以构造一个 表达式 。例如, ,可以在 之前添加 "" ,在 之前添加 '' ,然后串联起来得到表达式 "" 。返回可以通过上述方法构造的、运算结果等于 的不同表达式的数目。
提示
| 示例1 | 示例2 |
|---|---|
| 输入: , target = 输出: 解释: 一共有 种方法让最终目标和为 。 | 输入: , 输出: |
中规中矩的 0-1 背包
数组 中定义,
-
要添加 的元素集合,定义为
-
要添加 的元素集合,定义为
故有两个集合有如下推导关系,
其中, 是 所有元素之和。
与 一定是确定的,故两式相加/相减分别得,即
从题干条件获知一些边界情况。 元素均为非负整数,故 恒 ,同理 、 均 。我们总结几点已知“线索”:
-
一旦 集合确定,那么 集合也一定确定了
-
或 一定为偶数
-
的算数绝对值一定
-
如果 , 必须 (因为 ,所以 成立)
-
如果 , 必须 (因为 ,所以 成立)
-
当前问题可以转化成 0-1背包 问题:
1、元素不重复使用
2、背包容量为: 和 的较小值
1、确定 dp 数组及含义
💥 在 区间选取元素,这些元素和等于 时,共有 种方法。其中,
-
,
-
,
2、确定 dp 状态方程
如果当前元素已经大于元素和,即 ,肯定放弃 ,故有
如果当前元素不大于元素和,即 ,
-
如果主动不选取 ,故有
-
如果主动选取 ,故有
故,
3、确定 dp 初始状态
初始化: 在 区间选取元素(即,只能在 一个元素中选择),这些元素和等于 时,共有 种方法。
-
不选择 ,元素和也为 (一个元素也不选择),即
-
如果 的值恰为 时,那么 (有选择 、不选择 两种情况)
故,
初始化: 在 区间选取元素,这些元素和等于 时,共有 种方法。 的初始化要严格遵循上述定义的状态转移方程。从 开始,到 结束。
故,
初始化: 在 区间选取元素(即,只能在 一个元素中选择),这些元素和等于 时,共有 种方法。从 开始,到 结束,如果 恰与当前背包容量 相等,就有 种方案,否则为 。
故,
4、确定遍历顺序
参照经典二维背包遍历顺序,即,
-
第一层循环从 到
-
第二层循环从 到
5、确定返回值
在 区间选取元素,这些元素和等于 时,共有 。
6、示例代码
/**
* 空间复杂度 O(n * (target + sum)),n是nums数组长度,sum是nums元素之和
* 时间复杂度 O(n * (target + sum))
*/
function findTargetSumWays(nums: number[], target: number): number {
const sum = nums.reduce((p, v) => p + v, 0);
if (Math.abs(target) > sum || ((target + sum) & 1)) {
return 0;
}
// ----- 初始值准备开始 ------
const length = nums.length;
const bagWeight = Math.min((sum + target) >> 1, (sum - target) >> 1);
const dp = Array.from({ length }, () => new Array(bagWeight + 1).fill(0));
dp[0][0] = nums[0] === 0 ? 2 : 1;
for(let i = 1; i < length; i++) {
dp[i][0] = dp[i - 1][0] + (0 < nums[i] ? 0 : dp[i - 1][0 - nums[i]]);
}
for(let j = 1; j <= bagWeight; j++) {
dp[0][j] = j === nums[0] ? 1 : 0;
}
// ----- 初始值准备结束 ------
for(let i = 1; i < length; i++) {
for(let j = 1; j <= bagWeight; j++) {
dp[i][j] = dp[i - 1][j] + (j < nums[i] ? 0 : dp[i - 1][j - nums[i]]);
}
}
return dp[length - 1][bagWeight];
};
拓展方法(哨兵站岗)
上个方法中 状态初始化过于复杂:不仅要考虑 的非零状态, 与当前背包容量是否相等,对于 状态还要按照状态转移方程执行一遍,这些细枝末节都不利于面试场景。故我们对 含义做如下改动:
1、确定 dp 数组及含义
在 区间选取元素,这些元素和等于 时,共有 种方法。
其中,,,。
💥 在 项维度上向延伸了一个单位,故 的取值范围将从 变为 。
2、确定 dp状态方程
因为 维度延伸了一个单位,故在 数组中取值要格外小心,第 项对应的值为 ,故状态转移方程为,
3、确定 dp 初始状态
-
的初始化: 在[0,0)区间选取元素,这些元素和等于 时,共有 种方法,这个区间是没有元素的,故 。
-
的初始化:无须再初始化,按照状态转移方程遍历计算即可。
-
的初始化: 在[0,0)区间选取元素(这个区间是没有元素的),这些元素和等于 时,共有 种方法。只要 ,那么
综上所述,初始化 数组时,仅需要创建一个 的二维数组,并将 设置为 即可。
4、确定遍历顺序
-
第一层循环从 到
-
第二层循环从 到
5、确定返回值
在[0,n)区间选取元素,这些元素和等于 时,共有 种方法。
6、示例代码
/**
* 空间复杂度 O(n * (target +/- sum)),n是nums数组长度,sum是nums元素之和
* 时间复杂度 O(n * (target +/- sum))
*/
function findTargetSumWays(nums: number[], target: number): number {
const sum = nums.reduce((p, v) => p + v, 0);
if (Math.abs(target) > sum || ((target + sum) & 1)) {
return 0;
}
const n = nums.length;
const bagWeight = Math.min((sum + target) >> 1, (sum - target) >> 1);
const dp = Array.from({ length: n + 1 }, () => new Array(bagWeight + 1).fill(0));
dp[0][0] = 1;
for(let i = 1; i <= n; i++) {
for(let j = 0; j <= bagWeight; j++) {
dp[i][j] = dp[i - 1][j] + (j < nums[i - 1] ? 0 : dp[i - 1][j - nums[i - 1]]);
}
}
return dp[n][bagWeight];
};
状态压缩
/**
* 空间复杂度 O(target +/- sum),sum是nums元素之和
* 时间复杂度 O(n * (target +/- sum)),n是nums数组长度,
*/
function findTargetSumWays(nums: number[], target: number): number {
const sum = nums.reduce((p, v) => p + v, 0);
if (Math.abs(target) > sum || ((target + sum) & 1)) {
return 0;
}
const bagWeight = Math.min((sum + target) >> 1, (sum - target) >> 1);
const dp = new Array(bagWeight + 1).fill(0);
dp[0] = 1;
for(let i = 0, len = nums.length; i < len; i++) {
for(let j = bagWeight; j >= nums[i]; j--) {
dp[j] += dp[j - nums[i]];;
}
}
return dp[bagWeight];
};