A背包 DP

二维费用背包

两种费用同时受限

本课摘要

二维费用背包课程回答“同时受两种容量约束时,状态维度与枚举顺序如何设计”。内容以两种费用同时受限为主线,配合逐步推导、可编辑演示、例题与练习形成可复查的学习闭环。

  • 判断二维费用背包的适用条件与状态边界
  • 围绕“两种费用同时受限”推导转移与计算顺序
  • 用演示、复杂度分析和配套题目校验实现
本课目录 · 0 节

正在整理目录…

当一件东西,同时占两种资源

先看一个具体场景:你有 2 件物品,但背包这次卡的不是一条约束,而是两条, 物品 1 占「费用1 a=1a=1、费用2 b=2b=2」,价值 v=3v=3;物品 2 占「a=2,b=1a=2,b=1」,价值 v=4v=4。 背包要求:费用1 之和 ≤A=4\le A=4,同时费用2 之和 ≤B=4\le B=4。两条线都不能越界。

物品 1a=1b=2v=3物品 2a=2b=1v=4双约束背包费用1 ≤ A=4费用2 ≤ B=4
每件物品挂两个费用标签 (a, b);背包有两条互相独立的容量线(A 与 B),装入的物品要让两种费用之和都不超限。

这类约束在现实里遍地都是:买东西受钱和时间双重限制;装货受体积和质量双重限制;组队受预算和人数双重限制。 共同点是,每选一件,就要同时从两个「口袋」里各扣一笔,而且两个口袋互不相通:省下的时间换不来更多钱。

能不能只盯着一种费用做普通 01 背包,事后再检查另一种够不够?不行。因为「费用1 最省」的方案,费用2 未必也最省,两种费用的取舍是耦合的,必须一起进 DP 的状态,才知道某个费用1 的档位下、费用2 还剩多少空间。

状态与转移:给背包多开一维

回忆 01 背包的一维状态 f[j]f[j]:花费不超过 jj 时的最大价值。现在费用有两种,那就让状态同时记住两笔账: 设 dp[x][y]dp[x][y] 表示费用1 不超过 xx、费用2 不超过 yy 时能取得的最大价值。约束从一条数轴变成一整片平面,下标也从一个变成一对。

一维费用 · dp[j]01234一条约束:一个下标 j+1 维二维费用 · dp[x][y]两条约束:一对下标 (x, y)
一条费用 → 一个下标 j(数轴);两条费用 → 一对下标 (x, y)(平面)。二维费用不过是给 dp 增开一维。

转移和 01 背包一模一样的两条路,只是「扣费用」这一步要同时扣两种:

不取第 ii 件:它没参与,dp[x][y]dp[x][y] 保持原值(还没装它时的最优)。

取第 ii 件(前提两种费用都够:x≥aix\ge a_i 且 y≥biy\ge b_i):费用1 腾出 aia_i、费用2 腾出 bib_i,剩下的 (x−ai, y−bi)(x-a_i,\ y-b_i) 空间留给前面的物品去最优,再补上它的价值 viv_i,即 dp[x−ai][y−bi]+vidp[x-a_i][y-b_i]+v_i。

第 i 件 · 费用1 x · 费用2 ydp[x][y] = ?不取取(需 x ≥ a 且 y ≥ b)第 i 件没参与= dp[x][y](旧值)两种费用一起扣,补价值 v= dp[x−a][y−b] + v取较大者 = max(两者)
每格 dp[x][y] 仍是两条路取 max:不取则留原值;取则一次性扣掉两种费用 (x−a, y−b) 再补 v。

两条路取较大者,就得到转移方程(写成一维滚动数组的形式):

dp[x][y]=max⁡( dp[x][y], dp[x−ai][y−bi]+vi )dp[x][y]=\max\big(\,dp[x][y],\ dp[x-a_i][y-b_i]+v_i\,\big)

边界:dp[x][y]=0dp[x][y]=0(一件不装,价值为 0)。答案:dp[A][B]dp[A][B]。对照 01 背包一维式 f[j]=max⁡(f[j], f[j−wi]+vi)f[j]=\max(f[j],\ f[j-w_i]+v_i), 二维费用只是把「一个下标 jj、扣一种费用 ww」换成「两个下标 x,yx,y、同时扣两种费用 a,ba,b」,方程骨架分毫未动。

本质

二维费用不是新算法,而是给每件物品挂了两个属性标签:约束从一条变两条,DP 的状态维度就随之 +1。凡是「若干种相互独立的资源同时受限」,都照此把状态加一维即可,三种资源就加两维(时空代价会陡增,故通常止于二维)。

跟着算一遍

用开头的例子(物品 (a,b,v)=(1,2,3), (2,1,4)(a,b,v)=(1,2,3),\ (2,1,4),上限 A=B=4A=B=4)走几步,重点盯住每装一件,两种费用一起扣:

0
初始化整表。 一件都不装时,任何 (x,y)(x,y) 下价值都是 0:dp[⋅][⋅]=0dp[\cdot][\cdot]=0。这是二维表格的地基。
1
装入物品 1(a=1,b=2,v=3a=1,b=2,v=3)。凡是 x≥1x\ge1 且 y≥2y\ge2 的格,都能装下它:dp[x][y]=max⁡(0, dp[x−1][y−2]+3)=3dp[x][y]=\max(0,\ dp[x-1][y-2]+3)=3。于是表格「右下那一大片」(x≥1,y≥2x\ge1,y\ge2)全变成 3,其余仍是 0。
2
装入物品 2(a=2,b=1,v=4a=2,b=1,v=4)。看格 (x=2,y=2)(x{=}2,y{=}2):取 = dp[0][1]+4=0+4=4dp[0][1]+4=0+4=4,胜过原值 3 → dp[2][2]=4dp[2][2]=4(只装物品 2)。再看角落 (x=4,y=4)(x{=}4,y{=}4):取 = dp[2][3]+4=3+4=7dp[2][3]+4=3+4=7,胜过原值 3 → dp[4][4]=7dp[4][4]=7。
3
读答案。 dp[4][4]=7dp[4][4]=7,它对应「两件都装」:费用1 1+2=3≤41+2=3\le4、费用2 2+1=3≤42+1=3\le4,价值 3+4=73+4=7。两条约束同时满足,正是二维费用下的最优。
下面的演示把整张二维表逐件填出,高亮每件抬升了哪些格、来源在哪。改物品的 a,b,va,b,v 或两个上限,看表实时重算。

看它一件一件铺满平面

物品(每件两种费用 a / b 与价值 v · 可改可增删)
1
费用1 a
1
费用2 b
2
价值 v
3
2
费用1 a
2
费用2 b
1
价值 v
4
费用1 上限 A
A
4
费用2 上限 B
B
4
x=0
x=1
x=2
x=3
x=4
y=0
y=1
y=2
y=3
y=4
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
当前计算 依赖来源 被选转移 已确定
dp[x][y]=0dp[x][y] = 0
初始表:一件都不装时,任何 (费用1 x, 费用2 y) 下最大价值都是 0。行是费用2 y、列是费用1 x。
已暂停,第 1 步,共 4 步,1 倍速

注意演示里每处理一件,就把整张表刷一遍:能装下该件(x≥a, y≥bx\ge a,\ y\ge b)且更划算的格被抬高,其余不动。 这正是一维滚动写法的样子,只保留「当前这张二维表」,逐件在它上面就地更新。

用演示下方的按钮切到 「价值恒 1 · 数个数」模式:每件价值统一当 1,转移的 +vi+v_i 变成 +1+1,dp[x][y]dp[x][y] 就从「最大价值」变成「最多件数」,同一台机器,答案 dp[4][4]dp[4][4] 从 7(价值)变成 2(装得下两件)。这正是下面「变形一」讲的 P1855 那一路。

两个常见变形:数个数,与两维都倒序

变形一:价值恒 1,求「最多能选几件」。 很多题不问最大价值,而问「预算和时间都有限,最多能实现几个愿望 / 塞下几样东西」。 这只需把每件的价值统一设成 1,转移里的 +vi+v_i 变成 +1+1,dp[x][y]dp[x][y] 的含义就从「最大价值」变成「最多件数」:

dp[x][y]=max⁡( dp[x][y], dp[x−ai][y−bi]+1 )dp[x][y]=\max\big(\,dp[x][y],\ dp[x-a_i][y-b_i]+1\,\big)

「求个数」和「求价值」在背包里本是同一台机器,把价值当成 1 计,最大价值就是最多件数。下面例题 P1855 正是这一路。

变形二:把「二维」看成「朴素三属性物品」。 二维费用听着抽象,落到代码里不过是每件物品多带一个属性、循环多套一层。 像 P1507 那样「每份食物有体积、质量、卡路里」,体积和质量是两种费用,卡路里是价值,直接当普通 01 物品处理,只是背包状态是二维的 dp[j][k]dp[j][k] 而已。

至于循环方向:一维滚动写法里,两种费用维都要倒序(xx 从 AA 到 aa、yy 从 BB 到 bb)。道理和 01 背包「必须倒序」完全一致:倒序时 dp[x−a][y−b]dp[x-a][y-b] 用的是本件尚未装入的旧值,才能保证每件至多取一次。三层循环的骨架是「逐件 → 费用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 两个循环都必须倒序。哪怕只把其中一维写成正序,该维度上就会像完全背包那样「同一件被反复装入」,答案偏大。若题目本就允许每件取无限次(二维费用的完全型),才把两维都改成正序。

例题

P1855榨取 kkksc03洛谷原生普及-
题意
有 nn 个愿望,你有 MM 元钱与 TT 单位时间。实现第 ii 个愿望要花 mim_i 元、tit_i 时间。求在钱和时间都不超限的前提下,最多能实现几个愿望。
对应关系
「钱」= 费用1 aa(上限 A=MA=M),「时间」= 费用2 bb(上限 B=TB=T),每个愿望价值恒 1。二维费用最干净的入门题。
换个视角(价值恒 1 = 数个数)
不问价值、只问个数,把每件价值设为 1,dp[j][k]dp[j][k] 就是「花钱 ≤j\le j、花时间 ≤k\le k 时最多实现的愿望数」,转移的 +v+v 写成 +1+1。答案即 dp[M][T]dp[M][T]。
转移 · 复杂度
dp[j][k]=max⁡(dp[j][k], dp[j−mi][k−ti]+1)dp[j][k]=\max(dp[j][k],\ dp[j-m_i][k-t_i]+1),两维都倒序;时间 O(nMT)O(nMT)。
参考代码(二维 01,两维倒序)
#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;
}
P1507NASA 的食物计划洛谷原生普及/提高-
题意
飞船有体积上限 HH 和质量上限 TT。有 nn 种食物,第 ii 种占体积 hih_i、质量 tit_i,提供卡路里 cic_i,每种最多带一份。求携带食物的最大卡路里。
对应关系
「体积」= 费用1、「质量」= 费用2、「卡路里」= 价值。每份食物就是一个带两种费用、一个价值的普通 01 物品。
换个视角(二维 = 三属性物品)
把「二维背包」想成「物品有三个数:两笔费用 + 一份价值」,代码结构和一维 01 背包只差一层循环,外层逐份食物,内层是费用1、费用2 两个倒序循环。
转移 · 复杂度
dp[j][k]=max⁡(dp[j][k], dp[j−hi][k−ti]+ci)dp[j][k]=\max(dp[j][k],\ dp[j-h_i][k-t_i]+c_i),两维都倒序;时间 O(nHT)O(nHT)。
参考代码
#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 都可回炉自测(不看参考代码默写两维倒序的三层循环)。

P1509找啊找啊找 GF钱 + 人品双约束的二维费用背包:dp[j][k] 记「花钱 ≤ j、花人品 ≤ k」时能追到的最多女友数。难点在双关键字,先比女友数量最大,数量相同再比所花时间最少,转移时对这两个关键字依次取优。在洛谷打开
P1855榨取 kkksc03学完回来独立复现:钱、时间两种费用同时受限,价值恒 1 求最多愿望数。默写「逐件 → 钱倒序 → 时间倒序」的三层循环,答案取 dp[M][T]。在洛谷打开

已进入 二维费用背包 · 背包 DP · DP大师