目录
- 背包问题的本质
- 01 背包
- 完全背包
- 多重背包
- 混合背包
- 二维费用背包
- 分组背包
- 有依赖的背包
- 树形背包
- 背包方案数
- 输出具体方案
- 第 k 优解
- 可行性背包与 bitset 优化
- 恰好、不超过、至少
- 泛化物品背包
- 总结与刷题路线
1. 背包问题的本质
1.1 什么是背包问题
背包问题是一类在限制条件下做选择的优化问题。最经典的描述是:
有一个容量为
V的背包,有n件物品,每件物品有体积w[i]和价值v[i]。在不超过背包容量的前提下,选择一些物品,使得总价值最大。
这个模型可以套到很多实际场景:
- 采药:时间限制 = 背包容量,草药价值 = 物品价值
- 点菜:预算 = 背包容量,菜品价格 = 物品体积
- 选课:学分限制 = 背包容量,课程学分 = 物品价值
- 砝码称重:能称出的重量 = 背包容量,砝码 = 物品
1.2 为什么用动态规划
如果暴力枚举每件物品选或不选,复杂度是 O(2^n),n=100 就爆了。
背包问题的特点是:
- 有重叠子问题 :容量为
j时的最优解会被反复用到 - 有最优子结构:大容量的最优解可以由小容量的最优解推出
所以用 DP:把"容量"作为状态,逐步加入物品,更新每个容量下的最优值。
1.3 统一状态定义
绝大多数背包问题都可以用这个状态:
cpp
f[j] = 容量为 j 时能获得的最大价值
或者:
cpp
f[j] = 容量为 j 时的方案数
或者:
cpp
f[j] = 容量为 j 是否可达
区别只在于:
- 转移是取 max、求和还是取或
- 循环方向是正序还是逆序
- 初始化是什么
1.4 核心口诀
01 背包逆序,完全背包正序,分组背包逆序,树形背包合并。
为什么?后面每种类型都会详细解释。
2. 01 背包
2.1 问题描述
有 n 件物品,每件物品有体积 w[i] 和价值 v[i]。每件物品最多选一次 。背包容量为 V,求最大总价值。
2.2 状态设计
设:
cpp
f[j] = 容量为 j 时能获得的最大价值
2.3 朴素二维写法
先看最直观的二维 DP:
cpp
f[i][j] = 前 i 件物品,容量为 j 时的最大价值
转移:
- 不选第
i件:f[i][j] = f[i-1][j] - 选第
i件(前提j >= w[i]):f[i][j] = f[i-1][j-w[i]] + v[i]
取最大值:
cpp
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= V; j++) {
f[i][j] = f[i-1][j];
if (j >= w[i])
f[i][j] = max(f[i][j], f[i-1][j-w[i]] + v[i]);
}
}
答案:f[n][V]。
复杂度:时间 O(nV),空间 O(nV)。
2.4 一维滚动数组优化
观察转移:f[i][j] 只依赖 f[i-1][...],所以可以去掉第一维。
但如果直接写:
cpp
for (int i = 1; i <= n; i++)
for (int j = w[i]; j <= V; j++)
f[j] = max(f[j], f[j-w[i]] + v[i]);
这是错的 。因为正序时,f[j-w[i]] 可能已经被第 i 件物品更新过,导致第 i 件被选了多次,变成了完全背包。
正确写法是容量逆序:
cpp
for (int i = 1; i <= n; i++)
for (int j = V; j >= w[i]; j--)
f[j] = max(f[j], f[j-w[i]] + v[i]);
为什么逆序正确?
当计算 f[j] 时,f[j-w[i]] 还没有被第 i 件物品更新过,所以它代表的是"前 i-1 件物品在容量 j-w[i] 时的最优值"。这样第 i 件物品只会被用一次。
2.5 初始化
- 不超过容量 :
f[0..V] = 0,答案f[V] - 恰好装满 :
f[0] = 0,其他f[j] = -INF,答案f[V](若为-INF则无解)
为什么恰好装满要这样?因为 f[j] 表示"恰好容量为 j"。初始时只有容量 0 是"恰好装满"的,其他容量都不可达。
2.6 完整模板
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXV = 1005;
int f[MAXV];
int main() {
int n, V;
cin >> n >> V;
for (int i = 1; i <= n; i++) {
int w, v;
cin >> w >> v;
for (int j = V; j >= w; j--) {
f[j] = max(f[j], f[j-w] + v);
}
}
cout << f[V] << endl;
return 0;
}
2.7 常见坑
- 循环方向写反:01 背包必须逆序,写成正序就变成完全背包。
- 内层循环下界 :
j >= w[i],不能从 0 开始,否则j-w[i]越界。 - 恰好装满初始化 :别忘了
-INF。 - 多组数据 :每次要清空
f数组。
2.8 典型例题
- 洛谷 P1048 采药:裸 01 背包
- 洛谷 P1060 开心的金明:价值和体积互换
- 洛谷 P1164 小 A 点菜:01 背包方案数
- 洛谷 P1049 装箱问题:01 背包可行性
3. 完全背包
3.1 问题描述
有 n 种物品,每种物品有体积 w[i] 和价值 v[i]。每种物品可以选无限次 。背包容量为 V,求最大总价值。
3.2 状态设计
和 01 背包一样:
cpp
f[j] = 容量为 j 时能获得的最大价值
3.3 转移
完全背包的转移和 01 背包只差一个循环方向:容量正序。
cpp
for (int i = 1; i <= n; i++)
for (int j = w[i]; j <= V; j++)
f[j] = max(f[j], f[j-w[i]] + v[i]);
为什么正序就变成完全背包?
当计算 f[j] 时,f[j-w[i]] 可能已经被第 i 种物品更新过。也就是说,f[j-w[i]] 里可能已经包含了若干个第 i 种物品。再加上一个,就相当于第 i 种物品被选了多次。
3.4 和 01 背包的对比
| 01 背包 | 完全背包 | |
|---|---|---|
| 每件物品次数 | 最多 1 次 | 无限次 |
| 容量循环 | 逆序 V → w[i] |
正序 w[i] → V |
| 转移含义 | 用旧状态 | 用可能已更新的状态 |
3.5 初始化
和 01 背包一样:
- 不超过:全 0
- 恰好:
f[0]=0,其他-INF
3.6 完整模板
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXV = 10000005;
long long f[MAXV];
int main() {
int V, n;
cin >> V >> n;
for (int i = 1; i <= n; i++) {
int w, v;
cin >> w >> v;
for (int j = w; j <= V; j++) {
f[j] = max(f[j], f[j-w] + v);
}
}
cout << f[V] << endl;
return 0;
}
注意:完全背包的 V 可能很大(如 P1616 中 V=10^7),要用 long long。
3.7 常见坑
- 循环方向写反:完全背包必须正序,写成逆序就变成 01 背包。
- 数据范围 :完全背包的
V可能达到10^7,数组要开大,用long long。 - 和 01 背包混淆:记住口诀------01 逆序,完全正序。
3.8 典型例题
- 洛谷 P1616 疯狂的采药 :裸完全背包,
V很大 - 洛谷 P2722 总分 Score Inflation:完全背包
- 洛谷 P1853 投资的最大效益:完全背包 + 多次迭代
4. 多重背包
4.1 问题描述
有 n 种物品,每种物品有体积 w[i]、价值 v[i]、数量 c[i]。每种物品最多选 c[i] 次 。背包容量为 V,求最大总价值。
4.2 朴素做法
把每种物品的 c[i] 个都当成独立的 01 背包物品,做 c[i] 次 01 背包。
复杂度:O(V * sum(c[i]))。如果 c[i] 很大(如 10^9),会超时。
4.3 二进制拆分优化
核心思想:把 c 个相同物品拆成若干个"捆绑包",每个捆绑包的大小是 1, 2, 4, 8, ...,最后一个包是剩余数量。
例如 c = 13:
13 = 1 + 2 + 4 + 6
拆成 4 个包:1 个、2 个、4 个、6 个。用这些包可以组合出 0~13 任意数量。
证明:1, 2, 4 可以组合出 0~7;加上 6,可以组合出 6~13;合起来就是 0~13。
拆分代码:
cpp
int k = 1;
while (c >= k) {
// 用 k 个物品做一个 01 背包
zero_one(k * w, k * v);
c -= k;
k <<= 1;
}
if (c > 0) {
zero_one(c * w, c * v);
}
复杂度:O(V * sum(log c[i])),大大优化。
4.4 完整模板
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXV = 100005;
int f[MAXV];
void zero_one(int w, int v, int V) {
for (int j = V; j >= w; j--)
f[j] = max(f[j], f[j-w] + v);
}
int main() {
int n, V;
cin >> n >> V;
for (int i = 1; i <= n; i++) {
int w, v, c;
cin >> w >> v >> c;
int k = 1;
while (c >= k) {
zero_one(k * w, k * v, V);
c -= k;
k <<= 1;
}
if (c > 0)
zero_one(c * w, c * v, V);
}
cout << f[V] << endl;
return 0;
}
4.5 单调队列优化(进阶)
二进制拆分的复杂度是 O(V * sum(log c))。如果 c 很大但 n 也很大,还可以用单调队列优化到 O(nV)。
思路:对于每个物品,按容量模 w[i] 的余数分组,每组内用单调队列维护 f[j] - k*v 的最大值。
这个属于进阶内容,初学者先掌握二进制拆分即可。
4.6 常见坑
- 拆分时最后一个包 :
c剩余部分要单独处理。 - 数组大小 :二进制拆分后物品数量约为
sum(log c),数组要开够。 - 和完全背包区分 :如果
c[i] * w[i] >= V,可以当成完全背包处理,减少拆分。
4.7 典型例题
- 洛谷 P1776 宝物筛选:裸多重背包
- 洛谷 P2347 砝码称重:多重背包可行性 + bitset
- 洛谷 P1077 摆花:多重背包方案数
5. 混合背包
5.1 问题描述
有 n 种物品,每种物品可能是:
- 只能用 1 次(01)
- 只能用有限次(多重)
- 能用无限次(完全)
求容量 V 下的最大价值。
5.2 处理方法
分类处理:
- 01 物品:逆序 01 背包
- 完全物品:正序完全背包
- 多重物品:二进制拆分后做 01 背包
5.3 完整模板
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXV = 1005;
int f[MAXV];
void zero_one(int w, int v, int V) {
for (int j = V; j >= w; j--)
f[j] = max(f[j], f[j-w] + v);
}
void complete(int w, int v, int V) {
for (int j = w; j <= V; j++)
f[j] = max(f[j], f[j-w] + v);
}
int main() {
int n, V;
cin >> n >> V;
for (int i = 1; i <= n; i++) {
int w, v, c;
cin >> w >> v >> c;
if (c == 1) {
zero_one(w, v, V);
} else if (c == 0) {
// 假设 c=0 表示无限
complete(w, v, V);
} else {
int k = 1;
while (c >= k) {
zero_one(k * w, k * v, V);
c -= k;
k <<= 1;
}
if (c > 0)
zero_one(c * w, c * v, V);
}
}
cout << f[V] << endl;
return 0;
}
5.4 常见坑
- 类型判断 :题目中
c=0可能表示无限,也可能表示 0 个,要看题意。 - 完全背包和 01 背包的顺序:分类处理时不要混。
- 多重拆分的边界:同上。
5.5 典型例题
- 洛谷 P1833 樱花:混合背包经典题
- 洛谷 P1782 旅行商的背包:混合背包 + 泛化物品
6. 二维费用背包
6.1 问题描述
物品有两个费用维度,比如体积 w1[i] 和重量 w2[i]。背包有体积限制 V 和重量限制 M。每件物品有 1 个价值 v[i]。求最大价值。
6.2 状态设计
cpp
f[j][k] = 体积为 j、重量为 k 时的最大价值
6.3 01 二维费用
cpp
for (int i = 1; i <= n; i++)
for (int j = V; j >= w1[i]; j--)
for (int k = M; k >= w2[i]; k--)
f[j][k] = max(f[j][k], f[j-w1[i]][k-w2[i]] + v[i]);
两个维度都逆序,因为这是 01 背包。
6.4 完全二维费用
两个维度都正序:
cpp
for (int i = 1; i <= n; i++)
for (int j = w1[i]; j <= V; j++)
for (int k = w2[i]; k <= M; k++)
f[j][k] = max(f[j][k], f[j-w1[i]][k-w2[i]] + v[i]);
6.5 多重二维费用
二进制拆分后按 01 二维费用处理。
6.6 完整模板
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXV = 1005;
const int MAXM = 1005;
int f[MAXV][MAXM];
int main() {
int n, V, M;
cin >> n >> V >> M;
for (int i = 1; i <= n; i++) {
int w1, w2, v;
cin >> w1 >> w2 >> v;
for (int j = V; j >= w1; j--)
for (int k = M; k >= w2; k--)
f[j][k] = max(f[j][k], f[j-w1][k-w2] + v);
}
cout << f[V][M] << endl;
return 0;
}
6.7 常见坑
- 两个维度都要逆序:01 二维费用两个维度都逆序。
- 空间:二维数组可能很大,注意内存。
- 和分组背包混淆:二维费用是同一件物品有两个费用,分组背包是每组选一个。
6.8 典型例题
- 洛谷 P1855 榨取 kkksc03:二维费用 01 背包
- 洛谷 P1507 NASA 的食物计划:二维费用 01 背包
- 洛谷 P1759 通天之潜水:二维费用 + 输出方案
7. 分组背包
7.1 问题描述
有 n 件物品,分成若干组。每组内的物品最多选一个 。背包容量为 V,求最大价值。
7.2 状态设计
cpp
f[j] = 容量为 j 时的最大价值
7.3 转移
外层枚举组,中层枚举容量(逆序),内层枚举组内物品:
cpp
for (每组 g) {
for (int j = V; j >= 0; j--) {
for (auto [w, v] : g) {
if (j >= w)
f[j] = max(f[j], f[j-w] + v);
}
}
}
为什么容量要逆序?因为每组最多选一个,逆序保证每组只被选一次。
为什么组内枚举顺序无所谓?因为组内物品互斥,f[j-w] 用的是上一组的状态。
7.4 完整模板
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXV = 1005;
int f[MAXV];
vector<pair<int,int>> groups[105];
int main() {
int V, n;
cin >> V >> n;
int maxg = 0;
for (int i = 1; i <= n; i++) {
int w, v, g;
cin >> w >> v >> g;
groups[g].push_back({w, v});
maxg = max(maxg, g);
}
for (int g = 1; g <= maxg; g++) {
for (int j = V; j >= 0; j--) {
for (auto [w, v] : groups[g]) {
if (j >= w)
f[j] = max(f[j], f[j-w] + v);
}
}
}
cout << f[V] << endl;
return 0;
}
7.5 常见坑
- 容量逆序:分组背包必须逆序,否则每组可能选多个。
- 组内物品枚举顺序:无所谓,因为互斥。
- 空组:有些组可能没有物品,要处理。
7.6 典型例题
- 洛谷 P1757 通天之分组背包:裸分组背包
- 洛谷 P1064 金明的预算方案:有依赖的分组背包
- 洛谷 P1541 乌龟棋:多维 DP,类似分组
8. 有依赖的背包
8.1 问题描述
物品之间有依赖关系。比如选附件必须先选主件,选课必须先修先修课。
8.2 分类
- 依赖关系是森林:每个物品最多依赖一个物品,形成树。
- 依赖关系是 DAG:一般较复杂,竞赛中较少。
- 主件-附件模型:一个主件可以有多个附件,附件不再有附件。
8.3 主件-附件模型
对于每个主件,枚举它的附件组合:
- 不选主件
- 选主件
- 选主件 + 附件 1
- 选主件 + 附件 2
- 选主件 + 附件 1 + 附件 2
- ...
把每种组合看成一个"组",做分组背包。
8.4 完整模板(主件-附件)
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXV = 32005;
int f[MAXV];
int main() {
int V, n;
cin >> V >> n;
vector<int> w(n+1), v(n+1), belong(n+1, 0);
vector<vector<int>> attach(n+1);
for (int i = 1; i <= n; i++) {
int a, b, c;
cin >> a >> b >> c;
w[i] = a;
v[i] = a * b;
if (c == 0) {
belong[i] = i;
} else {
belong[i] = c;
attach[c].push_back(i);
}
}
for (int i = 1; i <= n; i++) {
if (belong[i] != i) continue;
// 枚举主件 i 的附件组合
vector<pair<int,int>> options;
int sz = attach[i].size();
for (int mask = 0; mask < (1 << sz); mask++) {
int tw = w[i], tv = v[i];
for (int k = 0; k < sz; k++) {
if (mask & (1 << k)) {
tw += w[attach[i][k]];
tv += v[attach[i][k]];
}
}
options.push_back({tw, tv});
}
// 分组背包
for (int j = V; j >= 0; j--) {
for (auto [tw, tv] : options) {
if (j >= tw)
f[j] = max(f[j], f[j-tw] + tv);
}
}
}
cout << f[V] << endl;
return 0;
}
8.5 常见坑
- 附件数量 :如果附件很多,
2^sz会爆炸。一般题目附件不超过 2 个。 - 归属关系:要正确处理主件和附件的编号。
- 价值计算:题目可能给的是价格和重要度,价值要相乘。
8.6 典型例题
- 洛谷 P1064 金明的预算方案:经典主件-附件
- 洛谷 P2014 选课:树形背包
9. 树形背包
9.1 问题描述
物品形成一棵树,选某个物品必须先选它的父物品。求容量 V 下的最大价值。
9.2 状态设计
cpp
dp[u][j] = 在 u 的子树中,且选了 u,容量为 j 时的最大价值
9.3 转移
对每个子节点 v,把 v 的子树合并到 u:
cpp
void dfs(int u) {
dp[u][w[u]] = v[u];
for (int v : g[u]) {
dfs(v);
for (int j = V; j >= w[u]; j--) {
for (int k = 0; k <= j - w[u]; k++) {
dp[u][j] = max(dp[u][j], dp[u][j-k] + dp[v][k]);
}
}
}
}
其中 dp[v][0] = 0 表示不选 v 这棵子树。
9.4 森林处理
如果有多棵树,加一个虚拟根 0,所有树的根都连到 0。虚拟根的 w=0, v=0。
9.5 完整模板
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 305;
const int MAXV = 305;
int n, V;
int w[MAXN], v[MAXN];
vector<int> g[MAXN];
int dp[MAXN][MAXV];
void dfs(int u) {
for (int j = w[u]; j <= V; j++)
dp[u][j] = v[u];
for (int v : g[u]) {
dfs(v);
for (int j = V; j >= w[u]; j--) {
for (int k = 0; k <= j - w[u]; k++) {
dp[u][j] = max(dp[u][j], dp[u][j-k] + dp[v][k]);
}
}
}
}
int main() {
cin >> n >> V;
for (int i = 1; i <= n; i++) {
int p;
cin >> w[i] >> v[i] >> p;
if (p == 0) g[0].push_back(i);
else g[p].push_back(i);
}
dfs(0);
cout << dp[0][V] << endl;
return 0;
}
9.6 复杂度优化
朴素树形背包是 O(nV^2)。可以用上下界优化:
- 上界:当前子树大小
- 下界:
w[u]
这样复杂度可以降到 O(nV)。
9.7 常见坑
- 虚拟根:森林要加虚拟根。
- 初始化 :
dp[u][j]初始为v[u],表示选了u。 - 循环边界 :
j >= w[u],k <= j - w[u]。
9.8 典型例题
- 洛谷 P2014 选课:树形背包经典
- 洛谷 P2015 二叉苹果树:树形背包
- 洛谷 P1273 有线电视网:树形背包 + 费用
10. 背包方案数
10.1 问题描述
求装满容量 V 的方案数,或者不超过容量 V 的方案数。
10.2 01 背包方案数
cpp
f[0] = 1;
for (int i = 1; i <= n; i++)
for (int j = V; j >= w[i]; j--)
f[j] += f[j-w[i]];
答案:f[V](恰好)或 sum(f[0..V])(不超过)。
10.3 完全背包方案数
cpp
f[0] = 1;
for (int i = 1; i <= n; i++)
for (int j = w[i]; j <= V; j++)
f[j] += f[j-w[i]];
10.4 多重背包方案数
二进制拆分后做 01 背包方案数。
10.5 完整模板
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXV = 10005;
int f[MAXV];
int main() {
int n, V;
cin >> n >> V;
f[0] = 1;
for (int i = 1; i <= n; i++) {
int w;
cin >> w;
for (int j = V; j >= w; j--)
f[j] += f[j-w];
}
cout << f[V] << endl;
return 0;
}
10.6 常见坑
- 初始化 :
f[0]=1,其他 0。 - 模数:方案数可能很大,要取模。
- 恰好 vs 不超过:答案不同。
10.7 典型例题
- 洛谷 P1164 小 A 点菜:01 背包方案数
- 洛谷 P1077 摆花:多重背包方案数
- 洛谷 P1832 A+B Problem:完全背包方案数
11. 输出具体方案
11.1 问题描述
不仅求最大价值,还要输出选了哪些物品。
11.2 二维记录法
开二维数组 dp[i][j] 和 choose[i][j]:
cpp
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= V; j++) {
dp[i][j] = dp[i-1][j];
if (j >= w[i] && dp[i-1][j-w[i]] + v[i] > dp[i][j]) {
dp[i][j] = dp[i-1][j-w[i]] + v[i];
choose[i][j] = true;
}
}
}
倒推:
cpp
int j = V;
for (int i = n; i >= 1; i--) {
if (choose[i][j]) {
cout << i << " ";
j -= w[i];
}
}
11.3 一维 + 记录法
如果只用一维,可以记录每个状态是从哪里转移来的:
cpp
int pre[MAXV]; // pre[j] 表示容量 j 是由哪个物品转移来的
但这样只能记录最后一个物品,要完整方案还是二维方便。
11.4 字典序最小方案
如果要求字典序最小,经典技巧:
- 物品倒序做 01 背包
- 从前往后贪心判断能不能选
具体:先倒序做一遍 DP,然后从第 1 个物品开始,如果 f[j] == f[j-w[i]] + v[i],就选它。
11.5 典型例题
- 洛谷 P1759 通天之潜水:二维费用 + 输出方案
- 洛谷 P1064 金明的预算方案:输出方案
12. 第 k 优解
12.1 问题描述
求容量为 V 时的第 1 到第 k 优价值。
12.2 状态设计
cpp
f[j][1..K] = 容量 j 的前 K 优解
12.3 转移
对每个物品,把 f[j] 和 f[j-w]+v 两个有序数组合并,取前 K 个。
01 背包逆序,完全背包正序。
12.4 完整模板
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXV = 5005;
const int MAXK = 55;
int f[MAXV][MAXK];
int a[MAXK], b[MAXK], c[MAXK];
int main() {
int K, V, n;
cin >> K >> V >> n;
memset(f, 0, sizeof(f));
for (int i = 1; i <= n; i++) {
int w, v;
cin >> w >> v;
for (int j = V; j >= w; j--) {
// 合并 f[j] 和 f[j-w]+v
for (int k = 1; k <= K; k++) {
a[k] = f[j][k];
b[k] = f[j-w][k] + v;
}
int x = 1, y = 1, z = 1;
while (z <= K && (x <= K || y <= K)) {
if (x <= K && (y > K || a[x] > b[y])) {
c[z++] = a[x++];
} else {
c[z++] = b[y++];
}
}
for (int k = 1; k <= K; k++)
f[j][k] = c[k];
}
}
int ans = 0;
for (int k = 1; k <= K; k++)
ans += f[V][k];
cout << ans << endl;
return 0;
}
12.5 常见坑
- 合并时去重 :如果
a[x] == b[y],只取一个。 - 初始化 :
f[0][1]=0,其他-INF。 - 复杂度 :
O(nVK)。
12.6 典型例题
- 洛谷 P1858 多人背包:第 k 优解经典
- POJ 3181 Dollar Dayz:方案数 + 高精度
13. 可行性背包与 bitset 优化
13.1 问题描述
只问"能不能凑出某个容量",不问最大价值。
13.2 bool 数组写法
cpp
bool f[MAXV];
f[0] = true;
for (int i = 1; i <= n; i++)
for (int j = V; j >= w[i]; j--)
f[j] = f[j] || f[j-w[i]];
13.3 bitset 优化
cpp
bitset<MAXV> f;
f[0] = 1;
for (int i = 1; i <= n; i++)
f |= (f << w[i]);
复杂度除以 64,非常快。
13.4 多重背包可行性
二进制拆分后做 01 可行性:
cpp
bitset<MAXV> f;
f[0] = 1;
for (int i = 1; i <= n; i++) {
int k = 1, c = cnt[i];
while (c >= k) {
f |= (f << (k * w[i]));
c -= k;
k <<= 1;
}
if (c) f |= (f << (c * w[i]));
}
13.5 典型例题
- 洛谷 P2347 砝码称重:多重背包可行性
- 洛谷 P1508 Likecloud-吃、吃、吃:可行性
14. 恰好、不超过、至少
14.1 恰好装满
- 初始化:
f[0]=0,其他-INF - 答案:
f[V] - 如果
f[V] = -INF,无解
14.2 不超过容量
- 初始化:
f[0..V]=0 - 答案:
f[V]
14.3 至少容量
方法一:扩大状态,把超过 V 的都当成 V。
cpp
for (int j = V; j >= w[i]; j--) {
int nj = min(V, j + w[i]);
f[nj] = max(f[nj], f[j] + v[i]);
}
方法二:定义 f[j] 为至少 j 的最大价值,转移时 min(V, j+w[i])。
14.4 方案数恰好 vs 不超过
- 恰好:
f[0]=1,答案f[V] - 不超过:
f[0]=1,答案sum(f[0..V])
14.5 典型例题
- 洛谷 P1049 装箱问题:不超过
- 洛谷 P1164 小 A 点菜:恰好方案数
- 洛谷 P1510 精卫填海:至少
15. 泛化物品背包
15.1 问题描述
物品的价值不是固定值,而是关于容量的函数 h(w)。
15.2 合并两个泛化物品
cpp
for (int j = 0; j <= V; j++)
for (int k = 0; k <= j; k++)
f[j] = max(f[j], h1[k] + h2[j-k]);
15.3 典型应用
- 物品价值随容量变化
- 多个物品组合成一个泛化物品
15.4 典型例题
- 洛谷 P1782 旅行商的背包:泛化物品
16. 总结与刷题路线
16.1 类型总结表
| 类型 | 限制 | 核心 | 复杂度 |
|---|---|---|---|
| 01 背包 | 每件 1 次 | 容量逆序 | O(nV) |
| 完全背包 | 每件无限 | 容量正序 | O(nV) |
| 多重背包 | 每件 c 次 | 二进制拆分 | O(V∑log c) |
| 混合背包 | 混合 | 分类处理 | 同上 |
| 二维费用 | 两个限制 | 二维逆/正序 | O(nVM) |
| 分组背包 | 每组最多 1 个 | 组外容量逆序 | O(V∑组内物品) |
| 树形背包 | 选子先选父 | 树形 DP 合并 | O(nV²) |
| 方案数 | 计数 | 加法转移 | O(nV) |
| 输出方案 | 具体选择 | 记录 + 倒推 | O(nV) |
| 第 k 优 | 前 k 优 | 有序表合并 | O(nVK) |
| 可行性 | 能否凑出 | bool / bitset | O(nV/64) |
16.2 刷题路线
入门:
- P1048 采药(01)
- P1616 疯狂的采药(完全)
- P1164 小 A 点菜(方案数)
进阶:
- P1776 宝物筛选(多重)
- P1833 樱花(混合)
- P1757 通天之分组背包(分组)
- P1064 金明的预算方案(依赖)
提高:
- P2014 选课(树形)
- P1858 多人背包(第 k 优)
- P2347 砝码称重(bitset)
综合:
- P1541 乌龟棋(多维 DP)
- P1782 旅行商的背包(泛化)
16.3 做题口诀
先看每件能用几次,再看有几个费用维度,再看有没有分组依赖,最后看求最大、方案数、可行性还是具体方案。
01 逆序,完全正序,分组逆序,树形合并。
16.4 最后的话
背包问题是动态规划的入门,也是竞赛中的常客。掌握这 15 种类型,你就能应对绝大多数背包题。
不要死记代码,要理解为什么这样转移。每道题都问自己:
- 状态是什么?
- 转移从哪来?
- 循环方向为什么是这样?
- 初始化为什么是这样?