【动态规划/背包问题】01 背包求方案数
前言
今天是我们讲解「动态规划专题」中的「背包问题」的第十九篇。
今天将学习「背包问题求方案数」问题。
另外,我在文章结尾处列举了我所整理的关于背包问题的相关题目。
背包问题我会按照编排好的顺序进行讲解(每隔几天更新一篇,确保大家消化)。
你可以先尝试做做,也欢迎你向我留言补充,你觉得与背包相关的 DP 类型题目 ~
题目描述
这是 LeetCode 上的「494. 目标和」,难度为「中等」。
Tag : 「DFS」、「记忆化搜索」、「背包 DP」、「01 背包」
给你一个整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :
- 例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
示例 1:
输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3
示例 2:
输入:nums = [1], target = 1
输出:1
提示:
- 1 <= nums.length <= 20
- 0 <= nums[i] <= 1000
- 0 <= sum(nums[i]) <= 100
- -1000 <= target <= 100
DFS
数据范围只有
,而且每个数据只有
两种选择,因此可以直接使用 DFS 进行「爆搜」。
而 DFS 有「使用全局变量维护」和「接收返回值处理」两种形式。
代码:
class Solution {
public int findTargetSumWays(int[] nums, int t) {
return dfs(nums, t, 0, 0);
}
int dfs(int[] nums, int t, int u, int cur) {
if (u == nums.length) {
return cur == t ? 1 : 0;
}
int left = dfs(nums, t, u + 1, cur + nums[u]);
int right = dfs(nums, t, u + 1, cur - nums[u]);
return left + right;
}
}
class Solution {
int ans = 0;
public int findTargetSumWays(int[] nums, int t) {
dfs(nums, t, 0, 0);
return ans;
}
void dfs(int[] nums, int t, int u, int cur) {
if (u == nums.length) {
ans += cur == t ? 1 : 0;
return;
}
dfs(nums, t, u + 1, cur + nums[u]);
dfs(nums, t, u + 1, cur - nums[u]);
}
}
- 时间复杂度:
- 空间复杂度:忽略递归带来的额外空间消耗。复杂度为
记忆化搜索
不难发现,在 DFS 的函数签名中只有「数值下标 u
」和「当前结算结果 cur
」为可变参数,考虑将其作为记忆化容器的两个维度,返回值作为记忆化容器的记录值。
由于 cur
存在负权值,为了方便,我们这里不设计成静态数组,而是使用「哈希表」进行记录。
代码:
class Solution {
public int findTargetSumWays(int[] nums, int t) {
return dfs(nums, t, 0, 0);
}
Map<String, Integer> cache = new HashMap<>();
int dfs(int[] nums, int t, int u, int cur) {
String key = u + "_" + cur;
if (cache.containsKey(key)) return cache.get(key);
if (u == nums.length) {
cache.put(key, cur == t ? 1 : 0);
return cache.get(key);
}
int left = dfs(nums, t, u + 1, cur + nums[u]);
int right = dfs(nums, t, u + 1, cur - nums[u]);
cache.put(key, left + right);
return cache.get(key);
}
}
- 时间复杂度:
- 空间复杂度:忽略递归带来的额外空间消耗。复杂度为
动态规划
能够以「递归」的形式实现动态规划(记忆化搜索),自然也能使用「递推」的方式进行实现。
根据记忆化搜索的分析,我们可以定义:
代表考虑前
个数,当前计算结果为
的方案数,令 nums
下标从
开始。
那么
为最终答案,
为初始条件:代表不考虑任何数,凑出计算结果为
的方案数为
种。
根据每个数值只能搭配
使用,可得状态转移方程:
到这里,既有了「状态定义」和「转移方程」,又有了可以滚动下去的「有效值」(起始条件)。
距离我们完成所有分析还差最后一步。
当使用递推形式时,我们通常会使用「静态数组」来存储动规值,因此还需要考虑维度范围的:
- 第一维为物品数量:范围为
nums
数组长度 - 第二维为中间结果:令
s
为所有nums
元素的总和(题目给定了nums[i]
为非负数的条件,否则需要对nums[i]
取绝对值再累加),那么中间结果的范围为
因此,我们可以确定动规数组的大小。同时在转移时,对第二维度的使用做一个 s
的右偏移,以确保「负权值」也能够被合理计算/存储。
代码:
class Solution {
public int findTargetSumWays(int[] nums, int t) {
int n = nums.length;
int s = 0;
for (int i : nums) s += Math.abs(i);
if (Math.abs(t) > s) return 0;
int[][] f = new int[n + 1][2 * s + 1];
f[0][0 + s] = 1;
for (int i = 1; i <= n; i++) {
int x = nums[i - 1];
for (int j = -s; j <= s; j++) {
if ((j - x) + s >= 0) f[i][j + s] += f[i - 1][(j - x) + s];
if ((j + x) + s <= 2 * s) f[i][j + s] += f[i - 1][(j + x) + s];
}
}
return f[n][t + s];
}
}
- 时间复杂度:
- 空间复杂度:
动态规划(优化)
在上述「动态规划」分析中,我们总是尝试将所有的状态值都计算出来,当中包含很多对「目标状态」不可达的“额外”状态值。
即达成某些状态后,不可能再回到我们的「目标状态」。
例如当我们的
不为
和
时,
和
就是两个对「目标状态」不可达的“额外”状态值,到达
或
已经使用所有数值,对
不可达。
那么我们如何规避掉这些“额外”状态值呢?
我们可以从哪些数值使用哪种符号来分析,即划分为「负值部分」&「非负值部分」,令「负值部分」的绝对值总和为
,即可得:
变形得:
问题转换为:只使用
运算符,从 nums
凑出
的方案数。
这样「原问题的具体方案」和「转换问题的具体方案」具有一一对应关系:「转换问题」中凑出来的数值部分在实际计算中应用
,剩余部分应用
,从而实现凑出来原问题的
值。
另外,由于 nums
均为非负整数,因此我们需要确保
能够被
整除。
同时,由于问题转换为 从 nums
中凑出
的方案数,因此「状态定义」和「状态转移」都需要进行调整(01 背包求方案数):
定义
为从 nums
凑出总和「恰好」为
的方案数。
最终答案为
,
为起始条件:代表不考虑任何数,凑出计算结果为
的方案数为
种。
每个数值有「选」和「不选」两种决策,转移方程为:
代码:
class Solution {
public int findTargetSumWays(int[] nums, int t) {
int n = nums.length;
int s = 0;
for (int i : nums) s += Math.abs(i);
if (t > s || (s - t) % 2 != 0) return 0;
int m = (s - t) / 2;
int[][] f = new int[n + 1][m + 1];
f[0][0] = 1;
for (int i = 1; i <= n; i++) {
int x = nums[i - 1];
for (int j = 0; j <= m; j++) {
f[i][j] += f[i - 1][j];
if (j >= x) f[i][j] += f[i - 1][j - x];
}
}
return f[n][m];
}
}
- 时间复杂度:
- 空间复杂度:
背包问题(目录)
- 01背包 : 背包问题 第一讲
- 完全背包 : 背包问题 第四讲
- 多重背包 : 背包问题 第八讲
- 多重背包(优化篇)
- 混合背包 : 背包问题 第十一讲
- 分组背包 : 背包问题 第十二讲
- 【练习】分组背包 : 背包问题 第十三讲
- 多维背包
- 树形背包 : 背包问题 第十六讲
- 背包求方案数
- 【练习】背包求方案数 : 本篇
- 【练习】背包求方案数
- 背包求具体方案
- 【练习】背包求具体方案
- 泛化背包
- 【练习】泛化背包
相关文章
- 前端用动态规划玩股票 - 最终章
- 动态规划:完全背包、多重背包[通俗易懂]
- 《算法图解》-9动态规划 背包问题,行程最优化
- java 动态规划 背包问题[通俗易懂]
- 蓝桥杯算法提高 促销购物(动态规划+完全背包)
- 一种使用工业机械臂稳定规划抓取 3D 可变形物体的方法
- 工业企业诊断和智能工厂规划
- 商业银行企业级IT架构规划
- 数字政府规划设计方案
- 动态规划01 背包问题(算法)
- 用javascript分类刷leetcode---动态规划(图文视频讲解)
- 【强烈推荐,讲座分享】IEEE计算智能学会演化调度与组合优化Taskforce系列讲座第7期:学习优化车辆路径规划问题
- 高效规划、跟踪和管理项目——探究Project 2019专业版的主要功能和优点
- 31省市数字化转型规划(2023)
- 跳跃-动态规划问题
- 蓝桥杯-本质上升序列(动态规划问题)
- 【运筹学】整数规划 ( 整数规划问题解的特征 | 整数规划问题 与 松弛问题 示例 )
- 机器人是如何规划路径的?动画演示一下吧
- 动态规划:背包问题
- 【算法】动态规划 ⑤ ( LeetCode 63.不同路径 II | 问题分析 | 动态规划算法设计 | 代码示例 )
- 【算法】动态规划 ⑥ ( 骑士的最短路径 II | 问题分析 | 代码示例 )
- 预计耗资近4亿美元的谷歌新园区规划曝光 重点发展智能硬件
- MySQL规划:教你如何设计高效数据库结构(mysql规划)