潜水员(二维费用的背包问题)

Description

潜水员为了潜水要使用特殊的装备。他有一个带2种气体的气缸:一个为氧气,一个为氮气。让潜水员下潜的深度需要各种的数量的氧和氮。潜水员有一定数量的气缸。每个气缸都有重量和气体容量。潜水员为了完成他的工作需要特定数量的氧和氮。他完成工作所需气缸的总重的最低限度的是多少?
例如:潜水员有5个气缸。每行三个数字为:氧,氮的(升)量和气缸的重量:
3 36 120
10 25 129
5 50 250
1 45 130
4 20 119
如果潜水员需要5升的氧和60升的氮则总重最小为249 (1,2或者4,5号气缸)。
你的任务就是计算潜水员为了完成他的工作需要的气缸的重量的最低值。

Input

从文本文件gas.in中读入数据。
第一行有2整数t,a(1<=t<=21,1<=a<=79)。它们表示氧,氮各自需要的量。
第二行为整数n (1<=n<=1000)表示气缸的个数。
此后的n行,每行包括ti,ai,wi(1<=ti<=21,1<=ai<=79,1<=wi<=800)3整数。这些各自是:第i个气缸里的氧和氮的容量及汽缸重量。

Output

<think>我们使用动态规划来解决二维背包问题。状态dp[i][j]表示至少获得i升氧气和j升气所需的最小重量。 注意:这里的状态是“至少”,意味着我们可以提供超过所需的氧气气,但重量要最小。 初始化:dp[0][0]=0,表示没有需求时重量为0;其他状态初始化为一个很大的数(表示不可达)。 然后,对于每个气缸(提供a升氧气,b升气,重量为c),我们更新状态: 对于当前状态(i, j),考虑使用这个气缸后,新的氧气量是i+a(但注意,如果i+a超过需求,我们只需要需求那么多,因为超过部分没有用,但我们可以用需求上限来避免不必要的计算。然而,在状态转移中,我们可以通过将超过需求的部分截断到需求上限来减少状态数。但题目要求至少满足,所以超过需求是可以的,但我们在状态表示中可以用不超过需求上限的状态来优化,但实际上我们只需要记录到需求上限即可,因为超过部分不会降低重量。 因此,我们可以这样设计: 状态转移方程:dp[i][j] = min(dp[i][j], dp[max(0, i - a)][max(0, j - b)] + c) 但是,注意:我们要求的是至少i和j,所以当我们加入一个气缸(提供a,b)时,那么之前的状态只需要满足至少i-a和j-b即可(如果i-a<0,那么之前状态可以是0,因为负数需求相当于0)。 然而,为了避免状态过多,我们可以将dp数组的大小设置为[m+1][n+1],然后从大到小更新(类似于背包问题的倒序更新,防止重复使用一个气缸)。 具体步骤: 1. 初始化dp数组,大小为(m+1) x (n+1),所有元素初始化为一个很大的数(比如INT_MAX/2),dp[0][0]=0。 2. 遍历每个气缸(k个): for (int i = m; i >= 0; i--) for (int j = n; j >= 0; j--) 如果当前状态dp[i][j]不是无穷大,那么考虑使用当前气缸后,新的氧气量 = i + a,新的气量 = j + b。 但是,由于我们只需要满足至少m和n,所以当i+a>m时,我们可以将其视为m(因为超过m的部分我们不需要,而且不会影响后续决策,因为后续决策只关心至少m和n);同样,当j+b>n时,视为n。 因此,新的状态为:ni = min(i+a, m), nj = min(j+b, n) 然后更新:dp[ni][nj] = min(dp[ni][nj], dp[i][j] + c) 但是注意:上面的顺序遍历会重复使用一个气缸?不对,因为我们是倒序遍历的(从大到小),所以每个气缸使用一次。 然而,另一种常见的做法是:将每个气缸看作一个物品,然后使用二维背包的模板。状态转移也可以这样: for (int i = m; i >= 0; i--) for (int j = n; j >= 0; j--) // 当前状态(i,j)可以由(i - a, j - b)转移而来,但要注意不能为负 int pre_i = max(0, i - a); int pre_j = max(0, j - b); dp[i][j] = min(dp[i][j], dp[pre_i][pre_j] + c); 两种方法都可以,但第二种更常见,而且更容易理解:即每个气缸使用一次,更新所有状态。 我们采用第二种方法,因为第一种方法在更新时,我们是从当前状态加上这个气缸去更新新状态,而第二种是从之前的状态加上这个气缸来更新当前状态。实际上,第二种方法更符合背包的常规思路。 步骤(第二种方法): dp[i][j]:表示至少需要i升氧气和j升气所需的最小重量。 初始化:dp[0][0]=0,其他为无穷大。 对于每个气缸(a, b, c): 从大到小遍历i从m到0,j从n到0: pre_i = max(0, i - a); // 表示在加入这个气缸之前,至少需要pre_i升氧气 pre_j = max(0, j - b); // 同理 dp[i][j] = min(dp[i][j], dp[pre_i][pre_j] + c); 最后输出dp[m][n] 注意:为什么倒序遍历?因为每个气缸只能用一次,正序会导致同一个气缸被重复使用(因为状态更新会依赖前面的状态,而前面的状态可能已经使用过该气缸)。倒序可以保证每个气缸只被使用一次。 例子:5升氧,60升气缸数据如上,我们期望得到249。 代码实现: 注意:由于m和n都不大(最大21和79),k最大1000,所以两层循环(m*n)是21*79=1659,再乘以k(1000)就是1659000,在可接受范围内。 代码:</think>### 解决方案:二维费用背包问题(01背包) #### 方法思路 这是一个经典的二维费用背包问题(01背包变种)。每个气缸可以看作一个物品,提供两种费用氧气气),并具有价值(重量)。目标是在满足至少所需氧气量(m)和气量(n)的前提下,最小化气缸总重量。 关键点: 1. **状态定义**:`dp[i][j]` 表示获得至少 `i` 升氧气和 `j` 升气所需的最小重量。 2. **状态转移**:对于每个气缸氧气量 `a`,气量 `b`,重量 `c`),更新状态: - `dp[i][j] = min(dp[i][j], dp[max(0, i - a)][max(0, j - b)] + c)` 3. **倒序更新**:确保每个气缸使用一次(01背包特性)。 4. **边界处理**:当 `i - a < 0` 或 `j - b < 0` 时,使用 `0` 作为索引(表示已满足该气体需求)。 #### 代码实现 ```cpp #include <iostream> #include <vector> #include <climits> #include <algorithm> using namespace std; int main() { int m, n; // 氧气气需求 cin >> m >> n; int k; // 气缸数量 cin >> k; // 初始化DP数组:dp[i][j] = 至少i升氧气和j升气的最小重量 vector<vector<int>> dp(m + 1, vector<int>(n + 1, INT_MAX)); dp[0][0] = 0; // 基础状态:0升氧气和0升气重量为0 // 处理每个气缸 while (k--) { int a, b, c; cin >> a >> b >> c; // 当前气缸氧气气、重量 // 倒序更新DP数组(01背包特性) for (int i = m; i >= 0; --i) { for (int j = n; j >= 0; --j) { // 跳过不可达状态 if (dp[i][j] == INT_MAX) continue; // 计算新状态索引(处理负数索引) int new_i = min(m, i + a); // 不超过需求上限 int new_j = min(n, j + b); // 更新新状态的最小重量 if (dp[new_i][new_j] > dp[i][j] + c) { dp[new_i][new_j] = dp[i][j] + c; } } } } cout << dp[m][n] << endl; return 0; } ``` #### 代码解释 1. **输入处理**:读取氧气需求 `m`、气需求 `n` 和气缸数量 `k`。 2. **DP数组初始化**: - 创建二维数组 `dp[m+1][n+1]`,初始值为 `INT_MAX`(表示不可达状态)。 - 设置基础状态 `dp[0][0] = 0`(无气体需求时重量为0)。 3. **气缸处理**: - 对于每个气缸,读取其氧气量 `a`、气量 `b` 和重量 `c`。 - **倒序更新状态**(从 `m` 到 `0`,`n` 到 `0`),确保每个气缸使用一次。 4. **状态转移**: - 计算新状态 `new_i = min(m, i + a)` 和 `new_j = min(n, j + b)`(避免超过需求上限)。 - 更新新状态的最小重量:`dp[new_i][new_j] = min(当前值, dp[i][j] + c)`。 5. **结果输出**:`dp[m][n]` 即为满足需求的最小重量。 #### 复杂度分析 - **时间复杂度**:O(k × m × n),其中 `k` 为气缸数量,`m` 和 `n` 为气体需求上限。 - **空间复杂度**:O(m × n),用于存储DP数组。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值