二维费用 0/1 背包:状态按需求上限收拢,倒序转移保证每个气缸只用一次,dp[m][n] 即两种气体均达标的最小总重。
OJ: roj
题目 ID: 1271
难度:普及+/提高-
标签:DP二维费用背包01背包
创建: 2026-09-30 02:37
更新: 2026-10-07 11:01
形式化题目
有
样例:
正解
思路
最直接的想法是枚举
- 需求是"至少":氧气超过
的部分毫无用处,因此不必记录真实气量, 只记录被需求收拢后的量——氧记作 、氮记作 。 状态数从"真实气量上界"降到 。 - 收拢后的转移是确定的:若收拢值
,实际氧气恰为 ,放入气缸后变成 ;若 ,实际氧气已达标,放多少仍是 。氮气同理。 所以转移只依赖收拢值本身:
于是得到二维费用 0/1 背包:状态
对应到代码:need_o、need_n(同时是状态表的两个维度),
收拢公式落在 ni = min(i + oxygen, need_o) 与 nj = min(j + nitro, need_n),
整个气缸的轮转移封装在 pack() 中。
两个细节决定正确性:
- 0/1 怎么保证:目标下标
总不小于当前下标, 所以把 、 都从大到小枚举——目标状态先被访问、当前状态后被访问, 本轮新写入的值不会再被读到,当前气缸不可能被用第二次;而读到的 在本轮尚未被改写,仍是"前若干个气缸"的结果。 - 同一收拢值为什么只留最轻的:未来转移只看收拢值,实际气量更多没有任何额外收益,
重量更小严格更好,所以逐轮取
不会丢掉最优子集。
下表展示样例按气缸逐轮推进的可达状态:第一列是已经处理完的气缸(一轮转移), 第二列枚举该轮结束后所有可达的收拢状态,写法为 $(i, j) = $ 最小总重, 表中没有出现的收拢状态即为不可达:
| 处理到的气缸 | 可达状态 $(i, j) = $ 最小总重 |
|---|---|
| 初始 | |
| 1 号 |
|
| 2 号 |
|
| 5 个气缸全部处理完 |
观察两点:第 2 轮中
代码
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-09-30 02:39
# update_at: 2026-09-30 02:39
import sys
INF = 10**9 # 不可达哨兵:最大总重 1000 × 800 = 8×10^5,远小于它
def pack(dp: list[list[int]], oxygen: int, nitro: int, weight: int) -> None:
"""放入一个气缸后的 0/1 转移:dp[i][j] 表示需求量被收拢到 (i, j) 时的最小总重。
需求是"至少"语义,状态下标按 min(i+oxygen, 上限) 收拢——超过需求的部分不再区分。
倒序枚举下标:目标状态只落在已访问过的位置,本气缸不会被二次使用;
而当前状态在本轮尚未被写过,读到的始终是"前 i-1 个气缸"的结果。
"""
need_o, need_n = len(dp) - 1, len(dp[0]) - 1
for i in range(need_o, -1, -1):
ni = min(i + oxygen, need_o)
row, nxt = dp[i], dp[ni]
for j in range(need_n, -1, -1):
cost = row[j] + weight
nj = min(j + nitro, need_n)
if cost < nxt[nj]:
nxt[nj] = cost
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
need_o, need_n = next(data), next(data) # 需要的氧气、氮气量
k = next(data) # 气缸个数
dp = [[INF] * (need_n + 1) for _ in range(need_o + 1)]
dp[0][0] = 0 # 什么都不带时两种气体都是 0,总重 0
for _ in range(k):
oxygen, nitro, weight = next(data), next(data), next(data)
pack(dp, oxygen, nitro, weight)
print(dp[need_o][need_n])
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-04 23:37
* update_at: 2026-10-04 23:37
*/
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long ll;
const int MAXM = 21; // 需要的氧气量上界
const int MAXN = 79; // 需要的氮气量上界
const int MAXK = 1005;
// dp[i][j]:需求收拢状态 (i, j)(氧气 min(实际, m)、氮气 min(实际, n))的最小总重
// 需求是"至少"语义,超过需求的气量不再区分,状态规模只有 (m+1)*(n+1)
// 最大总重 1000*800=8e5,int 足够;memset 0x3f 即为不可达哨兵
int dp[MAXM + 1][MAXN + 1];
ll need_o, need_n; // 题目要求的氧、氮量,同时是状态表的两个维度
ll k; // 气缸个数
ll oxygen[MAXK], nitro[MAXK], weight[MAXK]; // 每个气缸的氧量、氮量、重量
// 放入第 cur 个气缸的 0/1 转移
// 目标下标 (min(i+a, m), min(j+b, n)) 不小于当前下标 (i, j),
// 所以 i、j 都从大到小枚举:目标状态先写、当前状态后读,
// 本轮新写入的值不会被再次读到,每个气缸至多用一次(0/1 背包)
void pack(int cur) {
for (ll i = need_o; i >= 0; --i) {
ll ni = min(i + oxygen[cur], need_o); // 氧气收拢:超出需求记为 need_o
for (ll j = need_n; j >= 0; --j) {
ll nj = min(j + nitro[cur], need_n); // 氮气收拢:超出需求记为 need_n
ll cost = dp[i][j] + weight[cur];
if (cost < dp[ni][nj])
dp[ni][nj] = cost;
}
}
}
int main() {
scanf("%lld %lld", &need_o, &need_n);
scanf("%lld", &k);
for (int i = 1; i <= k; ++i)
scanf("%lld %lld %lld", &oxygen[i], &nitro[i], &weight[i]);
memset(dp, 0x3f, sizeof(dp)); // 不可达哨兵;最大总重 1000*800=8e5 远小于 0x3f3f3f3f
dp[0][0] = 0; // 什么都不带:两种气体均为 0,总重 0
for (int i = 1; i <= k; ++i)
pack(i);
printf("%d\n", dp[need_o][need_n]);
return 0;
}复杂度
- 时间:
—— 个气缸各一轮、每轮 个状态。 最坏 次内层操作,与 C++ 正解同阶, Python 下毫秒级完成。 - 空间:
,一张 的状态表,约 个整数。
总结
本题是"至少"语义的二维费用 0/1 背包。核心技巧是按需求上限收拢状态:
超出需求的气量不再区分,状态数被压到
图示解析
这张路线图串起本题从题面到答案的推导链:
选气缸子集,氧 >= m 且氮 >= n,总重最小
`- 朴素:枚举 2^k 个子集 → k=1000 指数爆炸
`- 观察①:需求是"至少",超出部分无信息 → 状态收拢为 (min(x,m), min(y,n))
`- 观察②:收拢值的转移只依赖收拢值本身 → 普通 min 转移即精确
`- 0/1 约束:目标下标只增不减 → i、j 倒序枚举
`- dp[0][0]=0 出发逐个气缸转移 → 答案 dp[m][n] = 249第一个分叉说明朴素枚举失败的原因是"整体枚举子集",没有可递推的状态;
观察①把二维状态压到