12 背包问题 — 01 背包

GESP C++ 六级:背包问题—01 背包

核心目标:掌握 01 背包的问题模型、二维 DP 推导、一维 DP 空间优化,理解“容量倒序遍历”的根本原因,能区分“最大价值”“恰好装满”“方案数”三种问法的初始化,能独立完成代码实现与真题变体。
01 背包 = 每件物品只能选一次;二维 DP 靠“选或不选”转移,一维 DP 靠“容量倒序”保证每件只用一次;不要求装满全初始化为 0,恰好装满只有 dp[0]=0,其余为 -INF。

一、考纲定位

根据 CCF 官方考纲,GESP 六级明确要求掌握“简单动态规划(一维动态规划、简单背包问题)”。01 背包是背包问题的基础,也是六级 DP 模块的高频考点。

01 背包在六级考试中的考查分布:

考点 重要程度 典型考法
01 背包问题模型 ★★★★★ 选择题、编程题
二维 DP 转移方程 代码填空、程序阅读
一维 DP 空间优化 代码填空高频
容量倒序遍历的原因 选择题高频
恰好装满的初始化 ★★★★☆ 编程题、代码填空
方案数统计 编程题
01 背包与完全背包区别 ★★★★★ 选择题高频

二、问题模型

有 n 件物品,第 i 件物品重量为 w[i],价值为 v[i]。背包容量为 W。每件物品最多只能选择一次(选或不选),求背包能装入的最大总价值。

关键约束:每件物品只有两种状态——选(1)或不选(0),因此称为 01 背包。

三、二维 DP 解法

3.1 状态定义

dp[i][j]:考虑前 i 件物品,背包容量为 j 时能获得的最大价值。

3.2 转移方程

对于第 i 件物品:

  • 不选:dp[i][j] = dp[i-1][j]
  • 选(前提 j >= w[i]):dp[i][j] = dp[i-1][j-w[i]] + v[i]

取两者最大值:

if (j >= w[i])
    dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i]);
else
    dp[i][j] = dp[i-1][j];

3.3 边界条件

dp[0][j] = 0,表示没有物品时价值为 0。

3.4 代码模板

int dp[105][10005];

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]);
        }
    }
}
cout << dp[n][W] << endl;

复杂度:时间 O(nW),空间 O(nW)。

四、一维 DP 空间优化(核心)

4.1 优化思路

观察二维转移方程,dp[i][j] 只依赖于 dp[i-1][...],即只依赖上一行。因此可以去掉第一维,用一维数组滚动更新。

4.2 一维代码模板

int dp[10005] = {0};

for (int i = 1; i <= n; i++) {
    for (int j = W; j >= w[i]; j--) {   // 关键:从大到小!
        dp[j] = max(dp[j], dp[j-w[i]] + v[i]);
    }
}
cout << dp[W] << endl;

复杂度:时间 O(nW),空间 O(W)。

4.3 为什么容量必须倒序遍历?

这是 01 背包最高频的选择题考点。

  • 倒序 j = W → w[i]:dp[j-w[i]] 尚未被第 i 件物品更新,代表的是“前 i-1 件物品”的状态。因此第 i 件物品只会被选一次。
  • 正序 j = w[i] → W:dp[j-w[i]] 可能已经在本次循环中被第 i 件物品更新过,导致第 i 件物品被重复选择,变成完全背包。

记忆口诀:01 背包倒着走,完全背包正着走。

4.4 二维与一维对比

对比项 二维 DP 一维 DP
状态 dp[i][j] dp[j]
空间 O(nW) O(W)
容量遍历 正序、倒序均可 必须倒序
可回溯方案 ✅ 可以 ❌ 需要额外记录
适用 需要输出方案 只求最大价值

五、三种常见问法与初始化

5.1 最大价值(不要求装满)

背包不要求恰好装满,求最大价值。

初始化:dp[j] = 0(所有容量初始为 0)。

int dp[10005] = {0};

5.2 恰好装满

要求背包恰好装满,求最大价值。

初始化:dp[0] = 0,其余 dp[j] = -INF。

const int INF = 1e9;
int dp[10005];
dp[0] = 0;
for (int j = 1; j <= W; j++) dp[j] = -INF;

// 转移同上
if (dp[W] < 0) cout << "无解" << endl;
else cout << dp[W] << endl;

原理:只有容量 0 能由“什么都不装”恰好达到;其他容量初始为 -INF,表示不可达。转移时只有可达状态才能更新。

5.3 方案数统计

求装满容量 W 的方案数。

初始化:dp[0] = 1,其余 dp[j] = 0。

int dp[10005] = {0};
dp[0] = 1;

for (int i = 1; i <= n; i++) {
    for (int j = W; j >= w[i]; j--) {   // 仍然倒序
        dp[j] += dp[j-w[i]];
    }
}
cout << dp[W] << endl;

六、输出方案(了解)

如果需要输出具体选了哪些物品,需要保留二维 DP 或额外记录选择。

回溯法:

int i = n, j = W;
while (i > 0 && j > 0) {
    if (dp[i][j] != dp[i-1][j]) {   // 选了第 i 件
        cout << i << " ";
        j -= w[i];
    }
    i--;
}

一维 DP 丢失了历史信息,无法直接回溯,需要二维数组或记录 choice。

七、01 背包与完全背包的区别

对比项 01 背包 完全背包
物品使用次数 每件最多 1 次 每件无限次
二维转移 dp[i-1][j-w] + v dp[i][j-w] + v
一维容量遍历 倒序 W → w[i] 正序 w[i] → W
典型题 采药、装箱 零钱兑换、无限背包

选择题高频:把 01 背包的容量循环改成正序,会变成什么?答案:完全背包。

八、典型例题与代码

例题:采药(01 背包模板)

题目:有 n 株草药,每株有采集时间 t[i] 和价值 v[i],总时间 T。每株草药只能采一次,求最大总价值。

#include <iostream>
using namespace std;

int main() {
    int T, n;
    cin >> T >> n;
    int t[105], v[105];
    for (int i = 1; i <= n; i++) {
        cin >> t[i] >> v[i];
    }

    int dp[1005] = {0};
    for (int i = 1; i <= n; i++) {
        for (int j = T; j >= t[i]; j--) {
            dp[j] = max(dp[j], dp[j-t[i]] + v[i]);
        }
    }
    cout << dp[T] << endl;
    return 0;
}

例题:装箱问题(求剩余空间最小)

题目:有一个箱子容量为 V,有 n 个物品,每个物品有体积。求箱子剩余空间的最小值。

转化:把体积同时看作重量和价值,求能装入的最大体积,答案 = V - dp[V]。

int dp[20005] = {0};
for (int i = 1; i <= n; i++) {
    for (int j = V; j >= v[i]; j--) {
        dp[j] = max(dp[j], dp[j-v[i]] + v[i]);
    }
}
cout << V - dp[V] << endl;

九、易错点汇总

  1. 一维 DP 容量正序遍历:导致物品被重复选择,变成完全背包。
  2. 物品循环与容量循环写反:01 背包必须物品在外层,容量在内层。
  3. 容量循环边界写错:j >= w[i],不能写成 j > w[i]。
  4. 恰好装满忘记初始化:dp[0]=0,其余 -INF,否则无解会被误判为 0。
  5. 方案数统计忘记倒序:方案数也要倒序,否则同一物品被重复计数。
  6. dp 数组未初始化:全局数组默认 0,局部数组需要手动初始化。
  7. 价值/容量溢出:大数用 long long。
  8. W 过大导致数组越界:数组大小要开到 W+1。
  9. 输出方案时一维 DP 无法回溯:需要二维 DP 或记录选择。
  10. “恰好装满”与“不超过容量”混淆:初始化不同,结果不同。

十、速查表

需求 写法 关键注意
二维 01 背包 dp[i][j]=max(dp[i-1][j], dp[i-1][j-w]+v) 依赖上一行
一维 01 背包 for(j=W; j>=w[i]; j--) dp[j]=max(dp[j], dp[j-w]+v) 倒序
最大价值 dp[j]=0 不要求装满
恰好装满 dp[0]=0,其余 -INF 最后判 dp[W]<0
方案数 dp[0]=1,dp[j]+=dp[j-w] 倒序
剩余空间最小 体积当价值,答案 V-dp[V] 转化
01 vs 完全 01 倒序,完全正序 选择题高频
时间复杂度 O(nW) 空间 O(W)

十一、总结

01 背包是 GESP 六级动态规划模块的核心模型。

  1. 01 背包每件物品只能选一次,选或不选。
  2. 二维 DP:dp[i][j] = max(dp[i-1][j], dp[i-1][j-w]+v)。
  3. 一维 DP 去掉第一维,空间从 O(nW) 降为 O(W)。
  4. 一维 01 背包容量必须从大到小遍历。
  5. 正序遍历会变成完全背包,这是选择题最高频考点。
  6. 不要求装满:dp 全初始化为 0。
  7. 恰好装满:dp[0]=0,其余 -INF。
  8. 方案数统计:dp[0]=1,倒序累加。
  9. 输出方案需要二维 DP 或额外记录。
  10. 多练“采药”“装箱问题”“恰好装满”“方案数”四类题。

复习建议:先把二维 01 背包的转移方程写熟,再动手改成一维并理解倒序的原因;然后重点练习“恰好装满”和“方案数”两种初始化;最后用“采药”和“装箱问题”两道经典题巩固。考试中先判断题目是最大价值、恰好装满还是方案数,再确定初始化方式,最后检查容量循环方向。