【动态规划】背包问题 - 二维费用的01背包问题
文章目录
- 1. 前言
- 2. 二位费用的01背包问题·
- 2.1_一和零
- 2.2_盈利计划
- 2.3_珠宝的最高价值
- 3. 似包非包问题
- 3.1_不同的二叉搜索树
- 3.2_组合总和Ⅳ
1. 前言
关于 动态规划的理解 与例题,点击👇
【动态规划】C++解决斐波那契模型题目(三步问题、爬楼梯、解码方法…)
有了上面的经验,我们来解下面 二维费用的 01 背包问题
2. 二位费用的01背包问题·
2.1_一和零

思路
-
初始化数据:
len是strs中字符串的数量。dp[i][j][k]表示在前i个字符串中,使用至多j个 ‘0’ 和k个 ‘1’ 能得到的最长子集长度。
-
统计每个字符串的 ‘0’ 和 ‘1’ 数量:
- 对于每个字符串,计算其包含的 ‘0’ 和 ‘1’ 数量,分别用
a和b表示。
- 对于每个字符串,计算其包含的 ‘0’ 和 ‘1’ 数量,分别用
-
动态规划状态转移:
- 不选择当前字符串,
dp[i][j][k] = dp[i-1][j][k]。 - 如果可以选择当前字符串(即
j >= a且k >= b),则更新状态为dp[i][j][k] = max(dp[i][j][k], dp[i-1][j-a][k-b] + 1),这表示选择当前字符串后的最长子集长度。
- 不选择当前字符串,
-
返回结果:
- 最终结果为
dp[len][m][n],即在所有字符串中,使用至多m个 ‘0’ 和n个 ‘1’ 能得到的最长子集长度。
- 最终结果为
这个算法通过三维 DP 表来处理每个子集的选择状态,确保在给定的 ‘0’ 和 ‘1’ 约束下,找到最大的子集长度。
代码
class Solution {
public:int findMaxForm(vector<string>& strs, int m, int n) {int len = strs.size();// 创建dp数组// dp[i][j][k]: 在前i个数中选,0的个数不超过j,1的个数不超过k,时的最长子集长度vector<vector<vector<int>>> dp(len+1, vector<vector<int>>(m+1, vector<int>(n+1)));for(int i = 1; i <= len; ++i){int a = 0, b = 0; // 统计当前字符串的01个数for(char ch : strs[i-1]) // 下标映射if(ch == '1') b++;else a++;// 填表for(int j = 0; j <= m; ++j)for(int k = 0; k <= n; ++k){dp[i][j][k] = dp[i-1][j][k]; // 不选择i字符串if(j >= a && k >= b)dp[i][j][k] = max(dp[i][j][k], dp[i-1][j-a][k-b]+1);}}return dp[len][m][n];}
};
2.2_盈利计划

思路
-
状态定义:
dp[i][j][k]表示使用前i个计划,选用的员工人数不超过j,总利润至少为k的方案数量。
-
初始化:
dp[0][j][0] = 1表示在没有选择任何计划的情况下,选择任意人数的员工,总利润可以被认为是0的情况有且仅有1种:即什么都不做。- 对于其他情况下,当选择0个计划时,所有人数的情况下利润都不能满足大于0的条件,所以这些状态也应初始化为0。
-
状态转移:
- 对于每个计划
i,考虑是否选择该计划:- 如果选择该计划,更新
dp[i][j][k],将其设为dp[i-1][j][k](不选择该计划的方案数)加上选择该计划后的方案数。 - 选择该计划后,员工人数减少
group[i-1],利润减少profit[i-1]。利润k需要用max(0, k - profit[i-1])来避免负利润的情况。
- 如果选择该计划,更新
- 更新后的方案数需要对
MOD取模,防止溢出。
- 对于每个计划
-
结果:
- 最终的答案是
dp[len][n][minProfit],即使用所有计划,员工人数不超过n,利润至少为minProfit的方案数量。
- 最终的答案是
复杂度分析
- 时间复杂度:
O(len * n * minProfit),其中len是计划数量,n是员工数量,minProfit是利润目标。 - 空间复杂度:
O(len * n * minProfit),需要一个三维的 DP 数组来存储状态。
代码
class Solution {
public:int profitableSchemes(int n, int minProfit, vector<int>& group, vector<int>& profit) {const int MOD = 1e9 + 7;int len = group.size();// 创建dp数组// dp[i][j][k]: 在前i个计划中选择,人数不超过j,利润至少为k 的选法数量vector<vector<vector<int>>> dp(len+1, vector<vector<int>>(n+1, vector<int>(minProfit+1)));// 初始化元素for(int j = 0; j <= n; ++j) dp[0][j][0] = 1;// 填表for(int i = 1; i <= len; ++i)for(int j = 0; j <= n; ++j)for(int k = 0; k <= minProfit; ++k){dp[i][j][k] = dp[i-1][j][k];if(j >= group[i-1]) // 下标映射dp[i][j][k] += dp[i-1][j-group[i-1]][max(0, k-profit[i-1])];dp[i][j][k] %= MOD;} return dp[len][n][minProfit];}
};
2.3_珠宝的最高价值

思路
-
初始化数据:
n是可用的总人数。minProfit是所需的最低利润。group和profit分别表示每个计划所需的人数和带来的利润。MOD是取模的常量,避免结果溢出。
-
创建 DP 表:
dp[i][j][k]表示在前i个计划中,选择方案使得使用的人数不超过j,并且获得的利润至少为k的选法数量。
-
初始化 DP 表:
dp[0][j][0]初始化为1,表示在没有任何计划时,无论人数j是多少,总是有1种方式(即选择什么都不做)。
-
动态规划状态转移:
- 不选择当前计划,
dp[i][j][k] = dp[i-1][j][k]。 - 如果选择当前计划(即
j >= group[i-1]),则从dp[i-1][j-group[i-1]][max(0, k-profit[i-1])]转移过来。这里的max(0, k-profit[i-1])确保即使利润k-profit[i-1]小于零,也不会影响 DP 状态。
- 不选择当前计划,
-
取模操作:
- 每次更新
dp表时,都取模MOD,确保结果不会因为大数而溢出。
- 每次更新
-
返回结果:
dp[len][n][minProfit]表示在所有计划中,使用最多n人,并且获得至少minProfit利润的选法数量。
这段代码通过三维 DP 表来追踪不同计划、人数和利润条件下的方案数量,确保最终计算出符合条件的所有方案数。
代码
class Solution {
public:int jewelleryValue(vector<vector<int>>& frame) {int m = frame.size(), n = frame[0].size();// dp的创建 与 元素初始化vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));for(int i = 1; i <= m; ++i)for(int j = 1; j <= n; ++j)dp[i][j] = max(dp[i-1][j], dp[i][j-1]) + frame[i-1][j-1]; // 映射下标(对于frame要-1)return dp[m][n];}
};
3. 似包非包问题
3.1_不同的二叉搜索树

思路
-
初始化 DP 数组:
dp[i]表示拥有i个节点的二叉搜索树的数量。dp[0]初始化为1,表示空树(即没有节点)视为一种有效的 BST。
-
填充 DP 表:
- 使用两层循环:
- 外层循环遍历所有节点数
i从1到n。 - 内层循环遍历每个节点
j作为根节点时的情况,j从1到i。 - 对于每个
j作为根节点,左子树有j-1个节点,右子树有i-j个节点。因此,dp[i]的更新公式是dp[i] += dp[j-1] * dp[i-j]。
- 外层循环遍历所有节点数
- 使用两层循环:
-
返回结果:
dp[n]就是拥有n个节点的所有可能的 BST 的数量。
代码
class Solution {
public:int numTrees(int n) {// 创建dp数组 dp[i]: 节点数为i时的二叉搜索树的种数vector<int> dp(n+1);// 初始化dp[0] = 1; // 空树算一种 / 为了后续填表正确// 填表for(int i = 1; i <= n; ++i)for(int j = 1; j <= i; ++j)dp[i] += dp[j-1] * dp[i-j];return dp[n];}
};
3.2_组合总和Ⅳ

思路
-
初始化 DP 数组:
vector<double> dp(target + 1);创建了一个 DP 数组dp,其中dp[i]表示和恰好为i的组合数。dp[0] = 1;初始化dp[0]为1,这是因为和为0的唯一组合是空组合。
-
动态规划填表:
- 遍历所有的
i从1到target。 - 对于每一个
i,遍历nums中的每个数x。- 如果
i >= x,那么i这个目标值可以由i-x这个目标值加上x得到。即,dp[i]可以从dp[i - x]递推过来。 - 因此,
dp[i] += dp[i - x];更新dp[i]的值。
- 如果
- 遍历所有的
-
返回结果:
return dp[target];最终返回dp[target],即所有可能的组合数,使得选取的数的和为target。
代码
class Solution {
public:int combinationSum4(vector<int>& nums, int target) {// 线性dp问题// 创建dp数组 dp[i]: 选择数使和恰好为i的组合总数vector<double> dp(target+1);dp[0] = 1; // 初始化for(int i = 1; i <= target; i++)for(int x : nums)if(i >= x)dp[i] += dp[i - x];return dp[target];}
};
