1. 引言
背包问题是动态规划领域最经典、最基础的一类问题,也是算法竞赛和面试中高频出现的考点。所谓「背包九讲」,是指围绕背包问题展开的九种经典变体与解法思路,从最简单的 01 背包,到完全背包、多重背包,再到分组背包、依赖背包等进阶模型。掌握这些模型,不仅能解决背包本身,更能帮助你建立动态规划的状态设计思维。
本文以 C++ 为语言载体,逐讲拆解九种背包问题的定义、状态转移方程、核心代码与优化技巧,并给出可直接运行的示例。
2. 第一讲:01 背包
01 背包是最基础的背包模型:有 N 件物品和一个容量为 V 的背包,每件物品只能选择放入一次(0 或 1 次),第 i 件物品的体积为 ci,价值为 wi,求在不超过背包容量的前提下能获得的最大总价值。
状态定义:dpj 表示容量为 j 的背包能装下的最大价值。核心转移方程为:
cpp
for (int i = 1; i <= N; i++) {
for (int j = V; j >= c[i]; j--) {
dp[j] = max(dp[j], dp[j - c[i]] + w[i]);
}
}
这里内层循环必须从大到小遍历,目的是保证每件物品只被使用一次,避免重复选取。
3. 第二讲:完全背包
完全背包与 01 背包的唯一区别在于:每件物品可以无限次选取。因此内层循环改为从小到大遍历,即可实现同一物品的多次累加。
cpp
for (int i = 1; i <= N; i++) {
for (int j = c[i]; j <= V; j++) {
dp[j] = max(dp[j], dp[j - c[i]] + w[i]);
}
}
两种背包代码只差一个循环方向,但语义完全不同,务必理解其背后的递推逻辑。
4. 第三讲:多重背包
多重背包中,第 i 件物品最多可选 ki 次。朴素做法是在 01 背包基础上再枚举选取数量,时间复杂度为 O(N·V·k)。更高效的做法是二进制拆分:把 ki 件物品拆成若干组,每组数量为 1、2、4、......、剩余部分,从而转化为 01 背包求解。
cpp
for (int i = 1; i <= N; i++) {
int num = k[i];
for (int p = 1; p <= num; p <<= 1) {
int cnt = min(p, num);
for (int j = V; j >= cnt * c[i]; j--) {
dp[j] = max(dp[j], dp[j - cnt * c[i]] + cnt * w[i]);
}
num -= cnt;
}
}
5. 第四讲:混合背包
混合背包是指同一问题中同时存在 01 背包、完全背包和多重背包的物品。处理方式很直接:遍历每件物品时,根据其类型选择对应的循环方向与拆分策略即可。
cpp
for (int i = 1; i <= N; i++) {
if (type[i] == 0) { // 01 背包
for (int j = V; j >= c[i]; j--)
dp[j] = max(dp[j], dp[j - c[i]] + w[i]);
} else if (type[i] == 1) { // 完全背包
for (int j = c[i]; j <= V; j++)
dp[j] = max(dp[j], dp[j - c[i]] + w[i]);
} else { // 多重背包,二进制拆分后按 01 处理
// 省略拆分细节
}
}
6. 第五讲:二维费用背包
二维费用背包是指每件物品同时占用两种资源(如体积和重量),背包也有两个容量上限。此时状态扩展为二维:dpjk 表示体积不超过 j、重量不超过 k 时的最大价值。
cpp
for (int i = 1; i <= N; i++) {
for (int j = V; j >= c[i]; j--) {
for (int k = M; k >= d[i]; k--) {
dp[j][k] = max(dp[j][k], dp[j - c[i]][k - d[i]] + w[i]);
}
}
}
7. 第六讲:分组背包
分组背包中,物品被划分为若干组,每组内最多只能选择一件。处理时先枚举组,再枚举容量,最后枚举组内物品,保证每组至多取一个。
cpp
for (int g = 1; g <= G; g++) {
for (int j = V; j >= 0; j--) {
for (int k = 0; k < group[g].size(); k++) {
int c = group[g][k].c, w = group[g][k].w;
if (j >= c) dp[j] = max(dp[j], dp[j - c] + w);
}
}
}
8. 第七讲:有依赖的背包
有依赖的背包是指某些物品必须在其「主件」被选取后才能选取,典型场景是树形结构。一般思路是:对每个主件,先对其附件做一次 01 背包,得到该主件在不同容量下的最优价值,再与主件合并,最后对所有主件做一次分组背包。
9. 第八讲:泛化物品与背包问题扩展
泛化物品是指价值随分配容量变化的物品,即 w = f(c)。这类问题可以统一用「泛化物品」的视角看待:每种背包模型本质上都是在容量与价值之间做最优分配。理解这一点,有助于把新问题抽象为已知模型。
10. 第九讲:背包问题方案总数与最优方案
除了求最大价值,背包问题还常要求统计方案总数或输出具体方案。方案总数只需把 max 改为求和:
cpp
dp[0] = 1;
for (int i = 1; i <= N; i++) {
for (int j = V; j >= c[i]; j--) {
dp[j] += dp[j - c[i]];
}
}
输出具体方案则需要在转移时记录选择路径,通常用二维数组 pathij 标记第 i 件物品在容量 j 下是否被选取,最后逆序回溯。
11. 总结与练习建议
背包九讲的核心在于理解「状态定义」与「循环顺序」背后的递推语义。建议按以下顺序练习:先吃透 01 背包与完全背包的代码差异,再掌握多重背包的二进制拆分,最后挑战分组与依赖背包。每道题都亲手写出状态转移方程,再对照代码验证,才能真正内化动态规划思想。
12. 10 道练习题(洛谷题号)
下面给出 10 道覆盖背包九讲各知识点的洛谷练习题,建议按难度循序渐进地完成:
- P1048 采药:01 背包入门题,适合用来熟悉状态转移方程与逆序循环。
- P1616 疯狂的采药:完全背包基础题,体会正序循环与 01 背包的差异。
- P1060 开心的金明:01 背包 + 价值加权,练习把题目条件转化为背包模型。
- P1049 装箱问题:01 背包变形,求剩余空间最小,训练逆向思维。
- P1164 小A点菜:方案总数类背包,对应第九讲内容,练习把 max 改为求和。
- P1776 宝物筛选:多重背包经典题,必须使用二进制拆分优化才能通过。
- P1833 樱花:混合背包综合题,同时包含 01、完全与多重背包,对应第四讲。
- P1855 榨取kkksc03:二维费用背包,对应第五讲,同时限制时间和金钱两种资源。
- P1757 通天之分组背包:分组背包模板题,对应第六讲,注意枚举顺序。
- P1064 金明的预算方案:有依赖的背包经典题,对应第七讲,主件与附件需合并处理。