二维费用背包
两种费用同时受限
本课摘要
二维费用背包课程回答“同时受两种容量约束时,状态维度与枚举顺序如何设计”。内容以两种费用同时受限为主线,配合逐步推导、可编辑演示、例题与练习形成可复查的学习闭环。
- 判断二维费用背包的适用条件与状态边界
- 围绕“两种费用同时受限”推导转移与计算顺序
- 用演示、复杂度分析和配套题目校验实现
本课目录 · 0 节
正在整理目录…
当一件东西,同时占两种资源
先看一个具体场景:你有 2 件物品,但背包这次卡的不是一条约束,而是两条, 物品 1 占「费用1 、费用2 」,价值 ;物品 2 占「」,价值 。 背包要求:费用1 之和 ,同时费用2 之和 。两条线都不能越界。
这类约束在现实里遍地都是:买东西受钱和时间双重限制;装货受体积和质量双重限制;组队受预算和人数双重限制。 共同点是,每选一件,就要同时从两个「口袋」里各扣一笔,而且两个口袋互不相通:省下的时间换不来更多钱。
能不能只盯着一种费用做普通 01 背包,事后再检查另一种够不够?不行。因为「费用1 最省」的方案,费用2 未必也最省,两种费用的取舍是耦合的,必须一起进 DP 的状态,才知道某个费用1 的档位下、费用2 还剩多少空间。
状态与转移:给背包多开一维
回忆 01 背包的一维状态 :花费不超过 时的最大价值。现在费用有两种,那就让状态同时记住两笔账: 设 表示费用1 不超过 、费用2 不超过 时能取得的最大价值。约束从一条数轴变成一整片平面,下标也从一个变成一对。
转移和 01 背包一模一样的两条路,只是「扣费用」这一步要同时扣两种:
不取第 件:它没参与, 保持原值(还没装它时的最优)。
取第 件(前提两种费用都够: 且 ):费用1 腾出 、费用2 腾出 ,剩下的 空间留给前面的物品去最优,再补上它的价值 ,即 。
两条路取较大者,就得到转移方程(写成一维滚动数组的形式):
边界:(一件不装,价值为 0)。答案:。对照 01 背包一维式 , 二维费用只是把「一个下标 、扣一种费用 」换成「两个下标 、同时扣两种费用 」,方程骨架分毫未动。
本质
二维费用不是新算法,而是给每件物品挂了两个属性标签:约束从一条变两条,DP 的状态维度就随之 +1。凡是「若干种相互独立的资源同时受限」,都照此把状态加一维即可,三种资源就加两维(时空代价会陡增,故通常止于二维)。
跟着算一遍
用开头的例子(物品 ,上限 )走几步,重点盯住每装一件,两种费用一起扣:
看它一件一件铺满平面
- 来源参与当前转移的依赖状态
- 当前正在计算或观察的状态
- 确定 / 最优已进入当前最优解或确定结果
- 已处理已经扫描、当前不再活跃的状态
- 非法越界、冲突或不可达状态
注意演示里每处理一件,就把整张表刷一遍:能装下该件()且更划算的格被抬高,其余不动。 这正是一维滚动写法的样子,只保留「当前这张二维表」,逐件在它上面就地更新。
两个常见变形:数个数,与两维都倒序
变形一:价值恒 1,求「最多能选几件」。 很多题不问最大价值,而问「预算和时间都有限,最多能实现几个愿望 / 塞下几样东西」。 这只需把每件的价值统一设成 1,转移里的 变成 , 的含义就从「最大价值」变成「最多件数」:
「求个数」和「求价值」在背包里本是同一台机器,把价值当成 1 计,最大价值就是最多件数。下面例题 P1855 正是这一路。
变形二:把「二维」看成「朴素三属性物品」。 二维费用听着抽象,落到代码里不过是每件物品多带一个属性、循环多套一层。 像 P1507 那样「每份食物有体积、质量、卡路里」,体积和质量是两种费用,卡路里是价值,直接当普通 01 物品处理,只是背包状态是二维的 而已。
至于循环方向:一维滚动写法里,两种费用维都要倒序( 从 到 、 从 到 )。道理和 01 背包「必须倒序」完全一致:倒序时 用的是本件尚未装入的旧值,才能保证每件至多取一次。三层循环的骨架是「逐件 → 费用1 倒序 → 费用2 倒序」:
for 每件物品 (a, v, b):
for x = A downto a: // ★费用1 维倒序
for y = B downto b: // ★费用2 维倒序
dp[x][y] = max( dp[x][y], dp[x − a][y − b] + v )记死:两维都倒序,缺一维就退化成完全背包
二维费用的 01 型,费用1 和费用2 两个循环都必须倒序。哪怕只把其中一维写成正序,该维度上就会像完全背包那样「同一件被反复装入」,答案偏大。若题目本就允许每件取无限次(二维费用的完全型),才把两维都改成正序。
例题
#include <iostream>
#include <algorithm>
using namespace std;
int m[205], t[205]; // 第 i 个愿望花的金钱 m、时间 t
int f[205][205]; // f[j][k]:花钱不超 j、花时间不超 k 时,最多实现的愿望数
int main()
{
int n, M, T;
cin >> n >> M >> T;
for (int i = 1; i <= n; i++)
cin >> m[i] >> t[i];
for (int i = 1; i <= n; i++) // 逐个愿望
for (int j = M; j >= m[i]; j--) // ★费用1(钱)倒序
for (int k = T; k >= t[i]; k--) // ★费用2(时间)倒序
f[j][k] = max(f[j][k], f[j - m[i]][k - t[i]] + 1); // 价值恒 1:数个数
cout << f[M][T] << endl;
return 0;
}#include <iostream>
#include <algorithm>
using namespace std;
int h[55], t[55], c[55]; // 第 i 份食物的体积 h、质量 t、卡路里 c
int f[405][405]; // f[j][k]:体积不超 j、质量不超 k 时的最大卡路里
int main()
{
int H, T;
cin >> H >> T;
int n;
cin >> n;
for (int i = 1; i <= n; i++)
cin >> h[i] >> t[i] >> c[i];
for (int i = 1; i <= n; i++) // 逐份食物(普通的三属性 01 物品)
for (int j = H; j >= h[i]; j--) // ★体积维倒序
for (int k = T; k >= t[i]; k--) // ★质量维倒序
f[j][k] = max(f[j][k], f[j - h[i]][k - t[i]] + c[i]);
cout << f[H][T] << endl;
return 0;
}练习
说明:纯二维费用的洛谷原生题目池并不宽。下面以 P1509 为主练一道「双费用 + 时间最少」的综合题;若想再练裸模板,上面的 P1855 / P1507 都可回炉自测(不看参考代码默写两维倒序的三层循环)。

