【动态规划 | 二维费用背包问题】二维费用背包问题详解:状态设计与转移方程优化
算法 | 相关知识点 | 可以通过点击 | 以下链接进行学习 | 一起加油! |
---|---|---|---|---|
斐波那契数列模型 | 路径问题 | 多状态问题 | 子数组 | 子序列 |
回文字串 | 01背包 | 完全背包 | 两个数组匹配问题 |
在经典的背包问题中,我们通常只需考虑物品的体积或重量这一种限制条件。然而,在实际应用中,约束条件往往更加复杂,例如同时受到容量和承重的限制,这就是二维费用背包问题。如何高效设计状态表示,并优化转移方程,成为解决此类问题的关键。本文将详细分析二维费用背包的动态规划解法,探讨状态设计的技巧,并进一步优化转移过程,帮助读者掌握这一经典模型的扩展与应用。
🌈个人主页:是店小二呀
🌈C/C++专栏:C语言\ C++
🌈初/高阶数据结构专栏: 初阶数据结构\ 高阶数据结构
🌈Linux专栏: Linux
🌈算法专栏:算法
🌈Mysql专栏:Mysql
🌈你可知:无人扶我青云志 我自踏雪至山巅
文章目录
- 474. 一和零
- 879. 盈利计划
- 377. 组合总和 Ⅳ
- 96. 不同的二叉搜索树
474. 一和零
【题目】:474. 一和零
【算法思路】
【初始化】
当字符串为空时,其长度应初始化为 0
。确保第一维的循环从小到大遍历,其余部分可按需处理。
【代码实现】
class Solution {
public:int findMaxForm(vector<string>& strs, int m, int n) {int len = strs.size();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;for(auto ch : strs[i - 1])if(ch == '0') a++;else b++;//填表操作for(int j = 0; j <= m; j++){for(int k = 0; k <= n; k++){dp[i][j][k] = dp[i - 1][j][k];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];}
};
【优化方案】
class Solution {
public:int findMaxForm(vector<string>& strs, int m, int n) {int len = strs.size();vector<vector<int>> dp(m + 1, vector<int>(n + 1));for(int i = 1; i <= len; i++){//统计字符个数int a = 0, b = 0;for(auto ch : strs[i - 1])if(ch == '0') a++;else b++;//填表操作for(int j = m; j >= a; j--){for(int k = n; k >= b; k--){dp[j][k] = max(dp[j][k], dp[j - a][k - b] + 1);}}} return dp[m][n];}
};
既然需要判断才进入该语句,不然将判断添加到循环当中,因为为01背包问题,遍历方向是从右往左的。
879. 盈利计划
【题目】:879. 盈利计划
【算法思路】
这道题从算法思路和题目分析角度来看都比较困难。通过对题目要求的深入分析,以及运用‘二维费用的动态规划经验’,我们可以得出合适的状态表示。在进行状态表示时,需要特别注意不超过:小于等于
和至少:大于等于
这两者之间的区别与含义。
根据最后一个位置进行情况分析时,我们需要考虑选择的不同情况。如果选择了 dp[i - 1][j - g[i]][k - p[i]]
,我们首先需要分析 j - g[i]
是否可能小于零。如果 j - g[i]
小于零,意味着总人数 j
会小于当前使用的 g[i]
,这就违反了限制条件,因此这种情况是不合法的。
接下来,我们分析 k - p[i]
在状态转移方程中的作用,这是本题的难点之一。我们需要判断 k - p[i]
是否可能小于0。如果小于0,意味着当前的最低利润要求 k
小于当前选择的项目 p[i]
的利润,这种情况是允许的。但问题在于,数组下标不能为负数,因此我们需要确保 k - p[i]
不会导致负的数组下标。
为了解决这个问题,我们可以使用 max(0, k - p[i])
,确保数组下标始终有效,并且为0作为最低限制。而在判断选择条件时,只需要保证 j >= g[i]
,即总人数 j
至少满足当前使用人数 g[i]
的要求。
“根据题目数据范围,需使用 MOD = 1e9 + 7
进行取模操作。在初始化方面,根据实际需求进行初始化。当任务和利润均为0时,人数是可变的,此时只有一种选择。而当没有任务时,利润为0,无论人数限制是多少,都能找到一个‘空集’方案。因此,我们将 dp[0][j][0]
初始化为 1,其中 0 <= j <= n
。
【代码实现】
class Solution {
public:int profitableSchemes(int n, int m, vector<int>& group, vector<int>& profit) {const int MOD = 1e9 + 7;int len = group.size();vector<vector<vector<int>>> dp(len + 1, vector<vector<int>>(n + 1, vector<int>(m + 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 <= m; 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][m];}
};
【优化方案】
class Solution {
public:int profitableSchemes(int n, int m, vector<int>& group, vector<int>& profit) {const int MOD = 1e9 + 7;int len = group.size();vector<vector<int>>dp (n + 1, vector<int>(m + 1));for(int j = 0; j <= n; j++) dp[j][0] = 1;for(int i = 1; i <= len; i++){for(int j = n ; j >= group[i - 1]; j--){for(int k = 0; k <= m; k++){dp[j][k] += dp[j - group[i - 1]][max(0, k - profit[i - 1])];dp[j][k] %= MOD;}}}return dp[n][m];}
};
377. 组合总和 Ⅳ
【题目】:377. 组合总和 Ⅳ
【算法思路】
首先,需要明确背包问题本质上是一个组合问题,且在有限条件下求解。因此,分析题目及示例后,我们可以发现这道题求的是排列数,而非组合数,所以不能简单地使用背包问题的解法。
对于此类问题,状态表示的设计并不是凭借经验或题目分析就能轻易得到的,而是需要通过分析问题的过程中,发现重复的子问题并加以抽象,从而得到合适的状态表示。
以四个元素排列为例,可以设定状态为“凑成总和i,有多少种排列方式”。通过分析问题,结合“目标值 - 当前元素”的思路,最终得出状态转移方程。
初始化 dp[0] = 1
的目的是为了确保后续状态转移的正确性,但是强行解释为和0,也有一种方式。
【代码实现】
class Solution {
public:int combinationSum4(vector<int>& nums, int target) {vector<double> dp(target + 1);dp[0] = 1;for(int i = 1; i <= target; i++){for(auto x : nums){if(i >= x) dp[i] += dp[i - x];}}return dp[target];}
};
96. 不同的二叉搜索树
【题目】:96. 不同的二叉搜索树
【算法思路】
这道题"经验 + 题目分析"很难得到状态表示,需要我们根据分析问题的过程中,发现重复子问题,抽象出来一个状态表示。
初始化 dp[0] = 1
的目的是为了确保后续状态转移的正确性,但是强行解释为和0,空树也是一种情况。
【代码实现】
class Solution {
public:int numTrees(int n) {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];}
};
快和小二一起踏上精彩的算法之旅!关注我,我们将一起破解算法奥秘,探索更多实用且有趣的知识,开启属于你的编程冒险!