🎒

0/1 背包

背包容量有限,每件宝贝"拿或不拿"——怎么选最值钱?

💡 打个比方

你要背一个限重 5 斤的背包去春游,柜子里有若干件宝贝,每件有重量和价值。宝贝不能切开:要么整件装进去(1),要么不装(0)——这就是"0/1"的来历。问题:怎么挑,让背包里宝贝的总价值最大?件数少时可以穷举(每件"拿/不拿"两种,共 2ⁿ 种组合),但 n 一大就爆炸——上 DP:对每件宝贝问一句"拿它,还是放下它",两种情况谁价值大听谁的。

注意区分:0/1 背包 = 每件最多拿一次;完全背包 = 同种宝贝可以拿多份(比如水可以买好几瓶)。本课只考 0/1。

0/1 背包:n 件物品,第 i 件重量 w[i]、价值 v[i];背包容量 W,每件至多拿一次,求总价值最大。状态 dp[i][j] = 只考虑前 i 件、容量 j 时的最大价值;转移 dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i])(不拿 / 拿)。

🖼️ 大图看明白

左边是"选宝贝"现场(容量 5,三件宝贝,最优选 2+3 号);右边是 DP 填好的二维表——答案就藏在最后一格 dp[3][5] = 9。

背包容量 W = 5:怎么选最值? 背包 容量 W = 5 物品 1:重 1 价 1 ✗ 不拿(不划算) 物品 2:重 2 价 4 ✓ 拿 物品 3:重 3 价 5 ✓ 拿 最优:拿物品 2 + 物品 3 重量 2+3 = 5(恰好装满),价值 4+5 = 9 带上物品 1 的组合价值最多 6,不如 9 DP 表:dp[i][j] = 前 i 件、容量 j 的最大价值 012345 i=0i=1i=2i=3 0 0 0 0 0 0 0 1 1 1 1 1 0 1 4 5 5 5 0 1 4 5 6 9 首行全 0:没有物品时只能装价值 0 dp[3][5] = 9 = 物品 2 + 物品 3(重 5 价 9) 0/1 背包:每件"拿 or 不拿"取大者;答案在最后一格 dp[n][W]

🧩 子任务 1:题目长什么样(穷举的极限)

🎯 小目标:会用穷举解小例子,说清为什么 n 大了必须上 DP

题目:n 件物品,第 i 件重量 w[i]、价值 v[i];背包容量 W。每件至多拿一次(拿 = 1,不拿 = 0),求背包内总价值最大。穷举:每件两种选择,共 2ⁿ 种组合。n=3 时 8 种,手算轻松;n=20 时 100 万种,电脑也吃力;n=50 直接放弃。GESP 的 n 可以到几百,必须 DP。

把 8 种组合全列出来(容量 5):

组合      总重量   总价值   合法?
{}        0       0        ✓
{1}       1       1        ✓
{2}       2       4        ✓
{3}       3       5        ✓
{1,2}     3       5        ✓
{1,3}     4       6        ✓
{2,3}     5       9        ✓ ← 最优
{1,2,3}   6       10       ✗ 超重
考点:0/1 = 每件至多一次(不可分割);完全背包 = 同件可拿多次。穷举 2ⁿ 只适合 n ≤ 20 左右,大 n 必须 DP。
✅ 小检查:8 种组合里有几种是"合法"的(总重量 ≤ 5)?{1,2,3} 价值 10 更高,为什么不能选?
看看答案
7 种合法。{1,2,3} 虽然价值 10,但总重量 1+2+3 = 6 > 5 超重了——背包题的价值再高,超了容量就是白搭。

🧩 子任务 2:状态与转移(拿 / 不拿 取大)

🎯 小目标:会定义 dp[i][j] 并推导出 max 转移方程

一问含义:dp[i][j] = 只考虑前 i 件物品、背包容量为 j 时,能装下的最大总价值。注意"只考虑前 i 件"——第 i+1 件以后的宝贝还没上架,不许偷看。二问转移:站在"第 i 件宝贝面前",只有两种选择:

  1. 不拿第 i 件:价值 = dp[i-1][j](容量原封不动,前 i-1 件的最优)。
  2. 拿第 i 件(前提是 j ≥ w[i],放得下):背里腾出 w[i] 的重量给前 i-1 件,价值 = dp[i-1][j - w[i]] + v[i]。

两种情况谁大听谁的:dp[i][j] = max(dp[i-1][j], dp[i-1][j - w[i]] + v[i])(j < w[i] 时只能不拿)。三问边界:第 0 行全 0(没有物品,价值就是 0)——不用特判,数组初始化为 0 即可。

为什么"拿"用的是 dp[i-1][j-w[i]] 而不是 dp[i][j-w[i]]:dp[i][j-w[i]] 里可能已经拿过第 i 件了——那就等于把同一件宝贝装了两遍(0/1 背包不许)。用 i-1 行,保证第 i 件只被考虑一次。
✅ 小检查:dp[2][3] 的值是几?它对应拿哪些物品?
看看答案
dp[2][3] = max(dp[1][3]=1, dp[1][1]+4 = 1+4 = 5) = 5:拿物品 1 和物品 2(重量 1+2 = 3 恰好装满,价值 1+4 = 5)。容量 3 放得下两件,比只拿物品 2(价值 4)更优。

🧩 子任务 3:二维代码 + 填表推演

🎯 小目标:会写二维 0/1 背包,能逐格解释表里每个数怎么来
// 0/1 背包(二维版):w/v 从 1 开始编号
int n = 3, W = 5;
int w[] = {0, 1, 2, 3};
int v[] = {0, 1, 4, 5};
vector<vector<int>> dp(n + 1, vector<int>(W + 1, 0));  // 边界:全 0

for (int i = 1; i <= n; i++) {
    for (int j = 0; j <= W; j++) {
        dp[i][j] = dp[i - 1][j];                 // 不拿第 i 件
        if (j >= w[i])                           // 放得下才考虑拿
            dp[i][j] = max(dp[i][j],
                           dp[i - 1][j - w[i]] + v[i]);  // 拿第 i 件
    }
}
printf("最大价值: %d\n", dp[n][W]);   // 9

抽查两个格子:dp[2][2] = max(dp[1][2]=1, dp[1][0]+4=4) = 4(只拿物品 2);dp[3][4] = max(dp[2][4]=5, dp[2][1]+5 = 1+5 = 6) = 6(拿物品 1+3,重 4 价 6)。最后一格 dp[3][5] = max(5, dp[2][2]+5 = 4+5 = 9) = 9(物品 2+3)。

考点:二维 0/1 背包时间 O(nW),空间 O(nW);外层物品、内层容量,转移永远看上一行(dp[i-1][*])。
✅ 小检查:dp[3][4] = 6 对应哪几件物品?如果 W 改成 4,答案变多少?
看看答案
dp[3][4] = 6 对应物品 1+3(重量 1+3 = 4,价值 1+5 = 6)。W=4 时答案就是 dp[3][4] = 6——容量小了 1,最优组合从 2+3 换成 1+3。

🧩 子任务 4:一维滚动优化——j 为什么倒着填

🎯 小目标:会写一维 0/1 背包,说清"倒序防重用"的道理

观察转移:dp[i][j] 只依赖 dp[i-1][*] 这一行——那一行就够,把"第 i 件"这轮更新直接叠在旧数组上:`dp[j] = max(dp[j], dp[j - w[i]] + v[i])`。但 j 必须倒着填(W → w[i])。道理:倒着走时,dp[j-w[i]] 还是上一轮(没含第 i 件)的值——第 i 件只用了一次;正着走,dp[j-w[i]] 可能已经被本轮更新过,等于把第 i 件装了两遍。

用物品 2(w=2, v=4)实测。此时 dp(处理完物品 1 后)= [0, 1, 1, 1, 1, 1]:

// 正着填(错):j = 2,3,4,5
j=2: dp[2] = max(1, dp[0]+4) = 4
j=3: dp[3] = max(1, dp[1]+4) = 5
j=4: dp[4] = max(1, dp[2]+4) = 8   // ← dp[2] 已含物品 2!等于拿了两次
j=5: dp[5] = max(1, dp[3]+4) = 9   // ← 同样拿重了

// 倒着填(对):j = 5,4,3,2
j=5: dp[5] = max(1, dp[3]+4) = 5
j=4: dp[4] = max(1, dp[2]+4) = 5   // dp[2] 还是旧值 1,只拿了一次
j=3: dp[3] = max(1, dp[1]+4) = 5
j=2: dp[2] = max(1, dp[0]+4) = 4

正填 dp[4] = 8(物品 2 装了两遍:4+4),倒填 dp[4] = 5 ✓(物品 1+2:1+4)。同一件宝贝"分身"了,价值自然虚高。

考点:一维 0/1 背包容量倒序(每件至多一次);完全背包容量正序(同件可多次,正序恰好允许"本轮的 dp[j-w[i]] 再被用")——方向就是 0/1 与完全的分别。
✅ 小检查:倒填物品 3(w=3, v=5)时,j=5 那一步用的是哪个旧值?dp[5] 最终是多少?
看看答案
处理完物品 2 后 dp = [0, 1, 4, 5, 5, 5]。倒填物品 3:j=5 时 dp[5] = max(5, dp[2]+5 = 4+5 = 9) = 9,用的是 dp[2]=4(旧值,含物品 1+2 的最优)。最终 dp[5] = 9,和二维表一致。

💻 完整代码

同一道题写两版:二维(好懂)+ 一维(省空间),答案必须一致。

// 0/1 背包:二维 + 一维
#include <bits/stdc++.h>
using namespace std;

int main() {
    int n = 3, W = 5;
    int w[] = {0, 1, 2, 3};
    int v[] = {0, 1, 4, 5};

    // 版本 1:二维
    vector<vector<int>> dp(n + 1, vector<int>(W + 1, 0));
    for (int i = 1; i <= n; i++)
        for (int j = 0; j <= W; j++) {
            dp[i][j] = dp[i - 1][j];
            if (j >= w[i])
                dp[i][j] = max(dp[i][j], dp[i - 1][j - w[i]] + v[i]);
        }
    printf("二维: %d\n", dp[n][W]);              // 9

    // 版本 2:一维(容量倒序)
    vector<int> f(W + 1, 0);
    for (int i = 1; i <= n; i++)
        for (int j = W; j >= w[i]; j--)          // 倒序!
            f[j] = max(f[j], f[j - w[i]] + v[i]);
    printf("一维: %d\n", f[W]);                   // 9(一致)
    return 0;
}
易错点:① 一维版内层循环 j >= w[i] 且递减,写成 j++ 正序就变成完全背包(同件拿多次)。② 一维数组全 0 初始化即可(容量 0 价值 0,边界天然成立)。③ 要输出"拿了哪些物品"得另存选择(从 dp[n][W] 往回追),一维滚动后追不回。

✋ 你来试试

第 1 题:W=3,物品 (1,2)、(2,3)、(1,3)(重量, 价值),最多价值多少?拿哪几件?
dp[1]: [0,2,2,2];dp[2]: j=2 → max(2, 0+3)=3,j=3 → max(2, 2+3)=5;dp[3]: j=1 → max(2, 0+3)=3,j=2 → max(3, 2+3)=5,j=3 → max(5, 3+3)=6。答案 6 = 物品 2+3(重量 2+1=3,价值 3+3=6)。
第 2 题:一维 0/1 背包里,内层循环为什么必须倒序?用一句话把"正序会怎样"说清楚。
倒序保证算 dp[j] 时,dp[j-w[i]] 还是上一轮(未含第 i 件)的值,第 i 件最多用一次;正序时 dp[j-w[i]] 可能已被本轮更新、已含第 i 件,再拿一次就"分身"了(变成完全背包)。
第 3 题(加餐):如果同种物品可以拿多份(完全背包),一维代码要改哪里?为什么?
只改内层循环方向:j 从 w[i] 到 W 正序。正序时 dp[j-w[i]] 可能是本轮刚更新的值(已含第 i 件),允许"再拿一份第 i 件",正好实现"可重复拿"。
🎯 一句话记住:0/1 背包 = 每件"拿 or 不拿"取大;dp[i][j] = max(不拿 dp[i-1][j],拿 dp[i-1][j-w]+v);一维版容量倒序防"分身"。