🎒 背包问题全家桶(从 0-1 出发,一网打尽)
学生请求:"把所有背包问题讲明白"。
核心思想:所有背包都是 0-1 背包的变形。抓住 0-1 的本质(倒序、每件一次),其他背包就是改一两个循环的事。
---
〇、背包问题家族地图(先看全景)
0-1 背包(每件最多 1 次)★ 根基
├─ 完全背包(每件无限次)→ 容量正序
├─ 多重背包(每件有限次 c 件)→ 二进制拆分转 0-1
├─ 混合背包(01 + 完全 + 多重混在一起)→ 分类处理
├─ 二维费用背包(重量+体积两个限制)→ dp 加一维
├─ 分组背包(每组最多选 1 件)→ 组外循环
└─ 依赖背包(买主件才能买附件)→ 分组背包思想/树形
规律:改"物品的使用规则"= 改遍历顺序或加维度;改"限制条件"= 加维度。
---
一、0-1 背包(复习,一切的基础)
问题:n 件物品,第 i 件重量 wᵢ、价值 vᵢ,容量 W,每件最多拿 1 次,求最大价值。
- 状态:
dp[j]= 容量 j 的最大价值 - 转移:
dp[j] = max(dp[j], dp[j-w] + v) - 顺序:容量倒序(保证每件只拿一次)
vector<int> dp(W + 1, 0);
for (int i = 0; i < n; i++) {
int w, v; cin >> w >> v;
for (int j = W; j >= w; j--) // 倒序!
dp[j] = max(dp[j], dp[j - w] + v);
}
为什么倒序:保证 dp[j-w] 是"没用过这件物品"的旧值。正序会让同一件被反复拿。
---
二、完全背包(每件无限次)
区别就一处:容量正序。
for (int i = 0; i < n; i++) {
int w, v; cin >> w >> v;
for (int j = w; j <= W; j++) // 正序!
dp[j] = max(dp[j], dp[j - w] + v);
}
为什么正序:正序时 dp[j-w] 已经被本件物品更新过,dp[j] = dp[j-w] + v 等于"再拿一件同样的"——相当于可以重复拿。
一句话:倒序 = 用过就没了(0-1);正序 = 随便用(完全)。
---
三、多重背包(每件有限次 c 件)
问题:第 i 件物品最多拿 cᵢ 件。
方法 1:朴素拆分(把 cᵢ 件拆成 cᵢ 个"单独的 0-1 物品")
vector<pair<int,int>> items; // {w, v}
for (int i = 0; i < n; i++) {
int w, v, c; cin >> w >> v >> c;
for (int k = 0; k < c; k++) items.push_back({w, v}); // 拆开
}
// 然后对 items 跑 0-1 背包
复杂度 O(W × Σcᵢ),c 大时会超时。
方法 2:二进制拆分(⭐ 比赛必用,O(W × Σlog cᵢ))
思想:任何数量 c,都能用 1, 2, 4, 8, ... 的组合表示(二进制)。比如 c=13:
拆成 1 + 2 + 4 + 6(13 = 1+2+4+6,最后一项是余数 13-7=6)。
这样 13 件物品变成 4 个"打包物品",每个打包物品还是 0-1 拿一次,但能组合出 0~13 任意数量!
vector<pair<int,int>> items;
for (int i = 0; i < n; i++) {
int w, v, c; cin >> w >> v >> c;
for (int k = 1; c > 0; k <<= 1) { // k = 1,2,4,8,...
int take = min(k, c);
items.push_back({take * w, take * v}); // take 件打包成 1 件
c -= take;
}
}
// 然后对 items 跑 0-1 背包(倒序)
验证:c=13 → take: 1, 2, 4, 6。用这 4 个打包件能组合出 1~13 的任意数量 ✅
---
四、混合背包(01 + 完全 + 多重都有)
思路:读入时给每件物品打个类型标签,按类型用不同的循环方向:
vector<tuple<int,int,int>> items; // {type, w, v} type: 0=01, 1=完全, 2=多重(c已拆好)
// 读入后:多重先二进制拆分转成 0-1;完全保留"正序"标签;0-1 保留"倒序"标签
// 处理时:
for (auto [type, w, v] : items) {
if (type == 0) { // 0-1:倒序
for (int j = W; j >= w; j--)
dp[j] = max(dp[j], dp[j-w] + v);
} else { // 完全:正序
for (int j = w; j <= W; j++)
dp[j] = max(dp[j], dp[j-w] + v);
}
}
本质:dp 数组只有一个,每个物品按自己的"使用规则"更新它,互不干扰。
---
五、二维费用背包(两个限制条件)
问题:每件物品有重量 w 和体积 t 两个限制,容量分别是 W 和 T,求最大价值。
思路:限制多一个 → dp 加一维。
- 状态:
dp[j][k]= 重量 j、体积 k 时的最大价值 - 转移:
dp[j][k] = max(dp[j][k], dp[j-w][k-t] + v) - 顺序:两个维度都倒序(0-1 性质)
vector<vector<int>> dp(W+1, vector<int>(T+1, 0));
for (int i = 0; i < n; i++) {
int w, t, v; cin >> w >> t >> v;
for (int j = W; j >= w; j--)
for (int k = T; k >= t; k--)
dp[j][k] = max(dp[j][k], dp[j-w][k-t] + v);
}
规律总结:每多一个限制,dp 就多一维,循环就多一层(都要倒序)。
---
六、分组背包(每组最多选 1 件)
问题:物品分成若干组,每组最多选 1 件,求最大价值。
思路:组的循环放最外层,组内物品放中间,容量倒序放最内层:
vector<vector<pair<int,int>>> groups; // 每组若干 {w, v}
for (auto &g : groups) { // ① 组在外
for (int j = W; j >= 0; j--) { // ② 容量倒序
for (auto [w, v] : g) { // ③ 组内物品
if (j >= w)
dp[j] = max(dp[j], dp[j - w] + v);
}
}
}
为什么组在外、容量在中:保证每组只"决策一次"(从组里选 0 或 1 件)。容量倒序保证组内物品不会互相叠加。
对比:如果容量放最外层,同一组的多件物品可能都被选中(错)。
---
七、依赖背包(买主件才能买附件)— 了解即可
问题:买相机(主件)才能买镜头(附件)。每组"主件 + 附件"选法有限(都不买 / 只买主件 / 主+附件组合)。
思路(分组背包思想):把每个主件及其附件的所有合法组合当成一组:
比如主件 A + 附件 a1 + a2,合法组合:
- 都不买(0 件)
- 只买 A
- 买 A + a1
- 买 A + a2
- 买 A + a1 + a2
把每个组合当成一个"物品"(重量=组合总重量,价值=组合总价值),然后跑分组背包(每组选 0 或 1 个组合)。
洛谷 P1064 金明的预算方案就是这个题,网络赛很少直接考,了解思想即可。
依赖背包参考代码(P1064 思路)
P1064 特点:每个主件最多 2 个附件,附件必须配主件。把每个主件+附件的所有组合枚举成"打包物品",再跑分组背包:
// 思路版代码(P1064:n 主件数、W 总钱数)
// group[i] = 第 i 个主件组的全部合法组合 {总价, 总价值}
// 组合枚举:都不买 → 只主件 → 主+附1 → 主+附2 → 主+附1+附2
vector<vector<pair<int,int>>> groups; // 每组若干组合
// ... 读入并枚举组合填入 groups ...
vector<int> dp(W + 1, 0);
for (auto &g : groups) { // 组在外
for (int j = W; j >= 0; j--) { // 容量倒序
for (auto [cost, val] : g) { // 组内组合
if (j >= cost)
dp[j] = max(dp[j], dp[j - cost] + val);
}
}
}
cout << dp[W] << endl;
和分组背包代码一模一样——区别只在"组内元素":分组背包放单件物品,依赖背包放"组合打包件"。
---
八、变式:背包求方案数 / 恰好装满
恰好装满(初始化技巧)
普通背包 dp 全 0(允许不满)。恰好装满时:dp[0]=0,其他 dp[j]=-INF(表示"装不满"),转移时 -INF + v 保持不可达:
vector<int> dp(W+1, -1e9);
dp[0] = 0;
// 转移同上,最后 dp[W] 若仍为 -1e9 则说明装不满
求方案数(加法转移)
价值改成"方案数",转移用 +:
vector<int> dp(W+1, 0);
dp[0] = 1; // 容量 0 有 1 种方案(啥都不装)
for (int i = 0; i < n; i++) {
int w; cin >> w; // 只关心重量(或同时带价值)
for (int j = W; j >= w; j--)
dp[j] += dp[j - w]; // 加方案数
}
对比记忆:求最大价值用 max,求方案数用 +,求最小费用用 min。
---
九、终极对比表(背这张表就够了!)
| 背包类型 | 物品规则 | 遍历顺序 | dp 维度 | 关键点 |
|----------|----------|----------|:---:|--------|
| 0-1 背包 | 每件最多 1 次 | 容量倒序 | 1 维 | 一切的基础 |
| 完全背包 | 每件无限次 | 容量正序 | 1 维 | 正序=可重复 |
| 多重背包 | 每件有限 c 次 | 二进制拆分后倒序 | 1 维 | 拆成 1,2,4,... |
| 混合背包 | 三种混一起 | 按类型分方向 | 1 维 | 分类处理 |
| 二维费用 | 重量+体积双限制 | 两维都倒序 | 2 维 | 限制多一维 |
| 分组背包 | 每组最多 1 件 | 组外→容量中→物品内 | 1 维 | 组循环在最外 |
| 依赖背包 | 主件才可带附件 | 组合枚举后分组 | 1 维 | 组合当物品 |
| 恰好装满 | 初始化 -INF | 同 0-1 | 1 维 | dp[0]=0 其余 -INF |
| 求方案数 | 转移用 + | 同 0-1 | 1 维 | dp[0]=1 |
---
十、学习路线建议(按顺序吃透)
- 0-1 背包 → P1048 采药(必做)
- 完全背包 → P1616 疯狂的采药(必做)
- 多重背包 → P1776 宝物筛选(二进制拆分,练手)
- 分组背包 → P1757 通天之分组背包
- 二维费用 → P1507 NASA的食物计划
- 依赖背包 → P1064 金明的预算方案(有余力再看)
吃透 1~4 就够网络赛拿分了,5~6 是进阶加分项。
---
十一、自测(答得上来 = 全家桶通关)
- 完全背包和 0-1 背包代码的唯一区别是什么?
- 多重背包 c=13 二进制拆分成哪几个数?为什么能表示 0~13 任意数量?
- 分组背包为什么组的循环要放最外层?
- 恰好装满和普通背包的初始化有什么区别?
- 二维费用背包比 0-1 多做了什么?
把答案或卡点发给我,我记录进档案并继续帮你。