混合背包
01/完全/多重同题
本课摘要
混合背包课程回答“01、完全和多重物品怎样共用一套转移框架”。内容以01/完全/多重同题为主线,配合逐步推导、可编辑演示、例题与练习形成可复查的学习闭环。
- 判断混合背包的适用条件与状态边界
- 围绕“01/完全/多重同题”推导转移与计算顺序
- 用演示、复杂度分析和配套题目校验实现
本课目录 · 0 节
正在整理目录…
同一道题里,三种「件数」并存
先看个小例子体会一下「为什么要分派」。容量 ,物品 1 是 01 件 ,物品 2 是完全件 。 物品 1 至多拿一次,物品 2 却能反复拿。如果对它俩用同一套循环方向,必然有一个出错, 要么把只有一件的物品 1 反复塞(当成了完全),要么把管够的物品 2 也锁死成一件(当成了 01)。
难点不在「想出新方程」,混合背包没有新方程。难点在于:不同物品的件数属性不同, 必须逐件判断它属于哪一类,再套用那一类的转移方式。这份「看属性、选方式」的对照,就是这一节的主角,分派表。
状态不变,靠「循环方向」分派
状态照旧。还是那条一维滚动数组: 表示容量不超过 时能取得的最大价值。 转移也照旧,就是那句熟得不能再熟的:
三类物品共用这一格 、共用这一句转移。它们唯一的差别,是怎么遍历容量 ,回想前几节反复强调的那件事:
为什么方向就能决定物种?算 要用到 :
01(恰一件)→ 倒序():此刻 还没被本件动过,是「这件还没进来」的干净旧值,于是本件至多被计入一次。
完全(无限件)→ 正序():此刻 可能已经含了本件,于是同一件能被反复叠加,正好表达「无限次」。
多重(有限件)→ 先拆再倒序:把 件二进制拆成 与余数几个「打包件」,每个打包件当一件普通 01 物品倒序处理。 拆分保证「取 0… 件」的每种可能都能凑出,倒序保证每个包至多用一次,合起来就是「不超过 件」。
本质 · 三类物品能同题混装,因为它们落在同一维 f[j] 上
混合背包不是一个新算法,而是前三节的拼装。既然 01、完全、多重最终都归结为同一句 , 就完全可以在同一个 上,对每件物品按其件数属性选择遍历方向 / 是否拆包,一件件叠加处理。谁先谁后都不影响结果,因为每件都只依赖「它进来之前」的 。
分派的骨架长这样
把分派表落成代码,主循环就是「逐件物品,看属性走对应分支」:
for 每件物品 (kind, w, v, m):
if kind == 01: // 恰一件
for j = W downto w: // 倒序
f[j] = max(f[j], f[j−w] + v)
elif kind == 完全: // 无限件
for j = w to W: // 正序
f[j] = max(f[j], f[j−w] + v)
else kind == 多重: // 有限 m 件
把 m 二进制拆成若干「打包件」(cnt·w, cnt·v)
for 每个打包件 (w', v'):
for j = W downto w': // 逐包倒序(当 01 物品)
f[j] = max(f[j], f[j−w'] + v')三条分支的循环体一字不差,区别只在 的方向与多重那一步的拆包。 把它们串在一个大循环里,一维 就地累积,处理完全部物品, 就是答案。这正是混合背包的定义式。
跟着算一遍:一件 01 + 一件完全
用开头的例子,物品 1 是 01 件 、物品 2 是完全件 ,容量 8。把两件先后落到同一维 上:
看三类物品落进同一维 f
- 来源参与当前转移的依赖状态
- 当前正在计算或观察的状态
- 确定 / 最优已进入当前最优解或确定结果
- 已处理已经扫描、当前不再活跃的状态
- 非法越界、冲突或不可达状态
一个统一写法:把 01 并进多重
实战里,「01」其实是「多重」的特例,恰一件,就是件数上限 的多重物品(二进制拆分后只有一个 包,逆序一遍,与 01 分毫不差)。 于是分派只需两条分支就够:
for 每件物品 (w, v, p): // p = 件数:0 表示无限
if p == 0: // 无限 → 完全,正序
for j = w to W: f[j] = max(f[j], f[j−w] + v)
else: // p ≥ 1(含 p==1 的“恰一件”)
把 p 二进制拆包,各包当 01 物品逆序处理这正是例题 P1833 樱花 的标准写法:题面用 编码件数, 是 01、 是多重、 是完全。 按 一分派,三类樱花就在同一维 上算完了。
例题
#include <iostream>
#include <algorithm>
using namespace std;
int f[1005]; // f[j]:用时不超过 j 的最大美观度
int w2[100005], v2[100005]; // 拆分后的「打包件」(有限件走这里)
int cnt; // 打包件总数
int main()
{
int sh, sm, eh, em, n; // 起止时刻 h:m 与樱花种数
scanf("%d:%d %d:%d %d", &sh, &sm, &eh, &em, &n);
int T = (eh - sh) * 60 + (em - sm); // 总时长(分钟)= 背包容量
for (int i = 1; i <= n; i++)
{
int w, v, p; // 花时间、美观度、件数上限 P
cin >> w >> v >> p;
if (p == 0) // ★P=0:无限件 → 完全背包,正序
{
for (int j = w; j <= T; j++)
f[j] = max(f[j], f[j - w] + v);
}
else // ★P>0:有限 P 件 → 多重,二进制拆分成打包件
{
int k = 1; // 1,2,4,… 各捆一包,余数单独成包
while (k < p)
{
cnt++;
w2[cnt] = k * w;
v2[cnt] = k * v;
p -= k;
k <<= 1;
}
if (p > 0)
{
cnt++;
w2[cnt] = p * w;
v2[cnt] = p * v;
}
}
}
for (int i = 1; i <= cnt; i++) // 有限件的打包件统一当 01 物品,逆序
for (int j = T; j >= w2[i]; j--)
f[j] = max(f[j], f[j - w2[i]] + v2[i]);
cout << f[T] << endl;
return 0;
}#include <iostream>
#include <algorithm>
using namespace std;
const int INF = 1e9;
int val[105], c[105]; // 第 i 种硬币面值、付款端持有数量
int fpay[100005]; // 付款端:凑「≥ 目标」的最少枚数(多重)
int fchg[100005]; // 找零端:凑「恰好」的最少枚数(完全)
int main()
{
int n, T; // 硬币种数、商品价格
cin >> n >> T;
int mx = 0; // 最大单面值(决定超付上界)
for (int i = 1; i <= n; i++) { cin >> val[i]; mx = max(mx, val[i]); }
for (int i = 1; i <= n; i++) cin >> c[i];
int LIM = T + mx * mx; // 付款可枚举到的上界(经典界)
for (int j = 1; j <= LIM; j++) fpay[j] = fchg[j] = INF;
// 付款端:硬币有限 → 多重背包,二进制拆分后逆序,求最少枚数
for (int i = 1; i <= n; i++)
{
int rest = c[i], k = 1;
while (rest > 0)
{
int t = min(k, rest); // 一包 t 枚
for (int j = LIM; j >= t * val[i]; j--)
if (fpay[j - t * val[i]] != INF)
fpay[j] = min(fpay[j], fpay[j - t * val[i]] + t);
rest -= t;
k <<= 1;
}
}
// 找零端:店家硬币无限 → 完全背包,正序,求最少枚数
for (int i = 1; i <= n; i++)
for (int j = val[i]; j <= LIM; j++)
if (fchg[j - val[i]] != INF)
fchg[j] = min(fchg[j], fchg[j - val[i]] + 1);
int ans = INF; // 枚举「实付 j 元、找零 j−T 元」
for (int j = T; j <= LIM; j++)
if (fpay[j] != INF && fchg[j - T] != INF)
ans = min(ans, fpay[j] + fchg[j - T]);
cout << (ans == INF ? -1 : ans) << endl;
return 0;
}练习
说明:纯「三类物品同题混装」的洛谷原生题目池很窄,真正综合的题多半把混合骨架藏进更大的模型里。因此这里用各分支的代表题组合覆盖:先用一道纯完全、一道纯有限件,把混合骨架的两条支路分别练熟,再回头做上面的 P1833 就水到渠成。

