0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1776,日期见页头。两边不一致时信原站。
题目描述
终于,破解了千年的难题。小 FF 找到了王室的宝物室,里面堆满了无数价值连城的宝物。
这下小 FF 可发财了,嘎嘎。但是这里的宝物实在是太多了,小 FF 的采集车似乎装不下那么多宝物。 看来小 FF 只能含泪舍弃其中的一部分宝物了。
小 FF 对洞穴里的宝物进行了整理,他发现每样宝物都有一件或者多件。他粗略估算了下每样宝物的价值,
之后开始了宝物筛选工作:小 FF 有一个最大载重为 W 的采集车,洞穴里总共有 n 种宝物,
每种宝物的价值为 vᵢ,重量为 wᵢ,每种宝物有 mᵢ 件。
小 FF 希望在采集车不超载的前提下,选择一些宝物装进采集车,使得它们的价值和最大。
输入格式
第一行为两个整数 n 和 W,分别表示宝物种数和采集车的最大载重。
接下来 n 行每行三个整数 vᵢ, wᵢ, mᵢ。
输出格式
输出仅一个整数,表示在采集车不超载的情况下收集的宝物的最大价值。
说明/提示
对于 30% 的数据,1 ≤ mᵢ,Σmᵢ ≤ 10⁴,0 ≤ W ≤ 10³,1 ≤ wᵢ, vᵢ ≤ 100。
对于 100% 的数据,1 ≤ mᵢ,Σmᵢ ≤ 10⁵,0 ≤ W ≤ 4 × 10⁴,1 ≤ n ≤ 100,
1 ≤ wᵢ, vᵢ ≤ 1000。
输入输出样例
输入
4 20 3 9 3 5 9 1 9 4 2 8 1 3
输出
47
载重 20,四种宝物 (v=3,w=9,m=3) (5,9,1) (9,4,2) (8,1,3)。
最优是拿两件第三种(价值 18、重量 8)+ 三件第四种(价值 24、重量 3)+ 一件第一种(价值 3、重量 9)
= 47,总重 20。
★ 这一组样例把本页四个错法全挡住了(51 / 38 / 160 / 36)。
这道题考什么:多重背包——每种物品有件数上限。正解把每种拆成 1, 2, 4, … 几堆跑 0/1 背包。
最容易错的两处
- 读错输入字段顺序:题面是价值 重量 件数,和大多数题的顺序不一样(和算法无关的一关)。
- 拆分错:把「1, 2, 4…, 剩余」写成别的(多拆一个或少拆一个)——两种错法方向相反,却栽在同一句话上,
m凑不齐0 .. m。 - 0/1 背包在二进制拆分上写成正序:件数上限直接失效,变成无界拾取。
能 AC 的:p1776.cpp(二进制拆分 + 0/1 背包内层倒序)这一版能 AC。
下面每一步里灰色的「深入」块是折叠的:那是这些结论怎么实测出来的,第一遍可以跳过。
深入:输入字段顺序(进阶)
绝大多数背包题给的是「重量 价值」,而这一道是 vᵢ wᵢ mᵢ —— 价值在前。
| 随机 300 轮:把两列读反,被抓的轮数 | 268 / 300 |
而「某一轮里每种的 v 恰好等于 w」(读反也没关系)的轮数 |
6 |
⇒ 它错得很凶(九成轮次),可它照样能通过你随手编的那一两组数据 ——
⚠ 而同一个错法在 P1164 的样例上是恒等变换(那道题的样例是 4 4)。
「样例挡不挡得住」的主语永远是那一组样例长什么样。
1第一版:把 m 件摊开成 m 件独立的物品
第 24 章第 ⑪ 步讲的就是它 —— 最直白,也一定对:
// P1776 第一版:把 m 件**摊开**成 m 件独立的物品,跑 01 背包//// [第 24 章第 ⑪ 步](/ch/24-knapsack-multi/)讲的就是它 —— 最直白,也一定对。//// ⚠ 复杂度 O(Σm × W),顶格是 10^5 × 4×10^4 = **4 × 10^9** ⇒ 必 TLE。//// ★★ 但它**不是零分**:题面给了一档 30% 的数据(Σm ≤ 10^4,W ≤ 10^3)// ⇒ 那一档只有 10^7 次,随便过。解析页第 ③ 步把这 30 分量出来了。// ⇒ **「暴力能拿多少分」是一道三十秒的算术题**,而不是「反正过不了就不写」。
#include <bits/stdc++.h>using namespace std;
int main() { int n, W; if (!(cin >> n >> W)) return 0; vector<int> f(W + 1, 0);
for (int i = 0; i < n; i++) { int v, w, m; cin >> v >> w >> m; for (int c = 0; c < m; c++) // ⚠ 一件一件地摊开 for (int j = W; j >= w; j--) f[j] = max(f[j], f[j - w] + v); } cout << f[W] << "\n"; return 0;}点「运行 ▶」看结果
复杂度 O(Σm × W),顶格是 10⁵ × 4 × 10⁴ = 4 × 10⁹ ⇒ 必 TLE。
深入:错法也有档可拿(进阶)
2★ 正解:二进制拆分
// P1776 宝物筛选 —— ★ 这一版就能 AC(二进制拆分 + 01 背包)//// 题意:载重 W,n 种宝物,第 i 种价值 v[i]、重量 w[i]、**有 m[i] 件**。求最大价值。//// ⚠⚠ 先说一个和算法无关的坑:**输入是「v w m」,价值在前、重量在后** ——// 而绝大多数背包题给的是「重量 价值」。读反了样例照样能过(见解析页第 ⑤ 步)。//// ★ 关键的一步:[第 24 章第 ⑫ 步](/ch/24-knapsack-multi/)的二进制拆分 ——// 把「m 件相同的宝物」拆成 1, 2, 4, …, 2^(k-1), 余数 这几堆,// 每堆当成**一件**新物品跑 01 背包。// 凭什么对:这几个数能拼出 0 .. m 里的**每一个**整数(二进制表示),一个不多一个不少。//// ⇒ 复杂度从 O(Σm × W) 变成 O(Σlog m × W)。// 顶格 Σm = 10^5、W = 4×10^4 ⇒ 4 × 10^9 变成 6.5 × 10^7(解析页第 ③ 步量了)。//// ⚠ 拆分时**别忘了最后那一堆余数** —— 忘了它就凑不出某些件数(解析页第 ④ 步,// 而那个 bug 有一个精确的触发条件:`m + 1 不是 2 的幂`)。
#include <bits/stdc++.h>using namespace std;
int main() { int n, W; if (!(cin >> n >> W)) return 0; vector<int> f(W + 1, 0);
for (int i = 0; i < n; i++) { int v, w, m; cin >> v >> w >> m; // ⚠ 价值 v 在前,重量 w 在后 for (int k = 1; m > 0; k <<= 1) { // 1, 2, 4, 8, … int take = min(k, m); // ★ 最后一堆是余数,不是 2 的幂 m -= take; int cw = w * take, cv = v * take; for (int j = W; j >= cw; j--) // 倒序:这一堆只用一次 f[j] = max(f[j], f[j - cw] + cv); } } cout << f[W] << "\n"; return 0;}点「运行 ▶」看结果
把「m 件相同的宝物」拆成 1, 2, 4, …, 2^(k−1),外加最后那一堆余数,
每堆当成一件新物品跑 01 背包。凭什么对:这几个数能拼出 0 .. m 里的每一个整数,
一个不多、一个不少(第 24 章第 ⑬ 步把这件事验给你看过)。
顶格(n = 100,每种 1000 件,W = 4 × 10⁴) |
|
|---|---|
| 拆出来的堆数 | 1000(每种 1000 件 ⇒ 10 堆) |
| 内层执行次数 | 22 572 928 |
| 本机毫秒 | 7 |
| 摊开要多少次 | 3 946 579 000 ⇒ ★ 175 倍 |
3★★★ 拆分的两个错法:方向相反,触发条件却是同一句话
正确 :1, 2, 4, …, 2^(k-1), 余数 加起来正好 m
拆多了 :1, 2, 4, …, 2^j (m 没减) 加起来 2^(j+1) − 1 ≥ m -> 能拿超过 m 件
拆少了 :1, 2, 4, …(余数丢了) 加起来 < m -> 有几件永远用不上
深入:两个 bug 栽在同一句话(进阶)
2^(j+1) − 1 正好等于 m 当且仅当 m + 1 是 2 的幂(m = 1, 3, 7, 15, 31, …)。
⇒ 那时余数是 0,两个错法都退化成正解。
造一档「每种的件数都取成 2^k − 1」的数据来验:
| 各 300 轮 | 默认档 | ★ m 全取 2^k − 1 |
|---|---|---|
| 「拆多了」被抓 | 81 | ★ 精确的 0 |
| 「拆少了」被抓 | 111 | ★ 精确的 0 |
「所有 m 都是 2^k−1」的轮数 |
75 | 300 |
★ 那两个 0 是结构性的(不是概率低),而自检就在同一张表里: 同一段对拍代码在默认档抓了 81 和 111 次。 ⇒ 和 P1439 / P2782 那次是同一个动作: 给「精确的 0」做自检,最省事的做法是换一个档位跑同一段代码。
⚠ 而这里的「触发条件」照例只是必要条件:默认档里 225 轮存在「m+1 不是 2 的幂」,
真被抓只有 81 / 111 —— 差 2.8 倍和 2.0 倍。
(件数拆错了,还得那几件真的用得上才会露馅。)
4★★ 错法三:内层写成正序 —— 件数上限整个失效
// P1776 错法二:把「有 m 件」当成了「有无限件」(完全背包,内层正序)//// 拆分那一层写对了,可内层方向写成正序 —— 于是每一堆都能重复取,件数上限整个失效。//// ★ 说清楚它算了什么:它的答案**恒等于「每种宝物有无限多件」的完全背包答案**// (解析页第 ⑤ 步量成 300 组逐组相等)。// ⇒ 于是它的触发条件也是白送的:**只有当件数限制真的卡住了最优解时它才错。**
#include <bits/stdc++.h>using namespace std;
int main() { int n, W; if (!(cin >> n >> W)) return 0; vector<int> f(W + 1, 0);
for (int i = 0; i < n; i++) { int v, w, m; cin >> v >> w >> m; for (int k = 1; m > 0; k <<= 1) { int take = min(k, m); m -= take; int cw = w * take, cv = v * take; for (int j = cw; j <= W; j++) // ⚠ 正序 —— 这一堆能被反复取 f[j] = max(f[j], f[j - cw] + cv); } } cout << f[W] << "\n"; return 0;}点「运行 ▶」看结果
| 随机 300 组:它的答案 vs 显式写的完全背包(每种无限件) | ★ 300 / 300 逐组相等 |
| 它被抓的轮数 | 171 / 300 |
⇒ 拆分那一层写得再对也没用:内层方向一错,件数上限就等于没写。 ★ 这是第 24 章第 ⑧ 步那句话的第三次现场 (P1616 是反过来的:完全背包写成倒序 ⇒ 变回 01 背包)。
5★★ 第 35 章补的一节:O(nW) 的单调队列 —— 而它在这道题上值不值钱要量
深入:章节间还账(进阶)
第 24 章讲完二进制拆分之后写着一句:「还能做到 O(nW),等第 35 章讲完单调队列再回来收尾。」
那笔账在第 35 章第 11 步还清了 —— 把转移里的 max 写出来:
f[j] = max over 0 ≤ t ≤ m of f[j − t·w] + t·v右边只用到「下标模 w 同余」的那一串。设 j = r + s·w、g[s] = f[r + s·w]:
g[s] = max over s−m ≤ s' ≤ s of ( g[s'] − s'·v ) + s·v★ 括号里那一坨只和 s' 有关 ⇒ 它就是「宽度 m+1 的滑动窗口最大值」,
正是第 35 章前半章那道题。每个容量只被处理一次 ⇒ O(nW),和 mᵢ 完全无关。
深入:更快但值不了钱(进阶)
第 35 章第 11 步那张表的结论是「交叉点在 k ≈ 100:件数少时二进制拆分更快,件数多时单调队列更快」。
这道题的顶格是 Σmᵢ = 10⁵ 分给 n = 100 种 ⇒ 每种 1000 件,稳稳在交叉点右边。
照理说单调队列该赢 —— 实测确实赢了,可赢得毫无意义:
A 机 · WSL2 · 8 核 / 7 G · 2026-08-31 · 独占 · 顶格 n = 100 / W = 4×10⁴ / Σm = 10⁵ |
二进制拆分 | 单调队列 |
|---|---|---|
| 内层步数(★ 机器无关的尺子) | 24 135 387 | 4 000 100 ⇒ 少 6.0 倍 |
| 秒表 | 9 ms | 5 ms ⇒ 快 1.8 倍 |
| 时限 1 秒 ⇒ 余量 | 111 倍 | 200 倍 |
⇒ ★★ 两条路都轻松过,差的那 4 毫秒买不到任何东西。
★ 而两把尺子又一次不一致(次数差 6.0 倍、秒表只差 1.8 倍)——
单调队列的每一步要维护双端队列、访问 f 还是跳着走的(P1074 那条)。
★ 题面 30% 那一档(Σm ≤ 10⁴、W ≤ 10³)更是分不出高下:122 322 步 vs 100 100 步。
⇒ ★★★ 于是这道题给出的结论和第 34 章 P1546(n = 100 的稠密图上
「朴素 Prim 更快」一分不值)是同一个形状:
「这个技巧在这道题上成立」和「这道题该用它」是两句话。
⚠ 这道题该用的仍然是二进制拆分 —— 它短得多,也不容易写错
(本页第 ③④ 步那三个错法,单调队列版一个都不会有;但它自己有别的,见第 35 章第 11 步那个「读要在写之前」)。
深入:摊开这一版真过不去(进阶)
顶格 Σm = 10⁵、W = 4×10⁴ |
件数 | 内层量级 |
|---|---|---|
摊开成 Σm 件(第 ① 步) |
100 000 | 4 × 10⁹ ⇒ 跑不完 |
| 二进制拆分 | ★ 1 000(少 100 倍) | 2.4 × 10⁷ |
| 单调队列 | —(不拆) | 4 × 10⁶ |
★ 所以这道题在第 35 章题单里的位置是「O(nW) 的用武之地」——
⚠ 而实测把话说得更准了:是「用得上」,不是「非用不可」。
6参照物、度量程序和生成器
7一页纸
| 关键的一步 | 二进制拆分:m 件拆成 1,2,4,…,2^(k−1) + 余数,每堆当一件跑 01 背包 |
| 哪一版能 AC | p1776.cpp —— 顶格 1000 堆 / 2257 万次 / 7 毫秒 |
| 第一版值多少分 | 摊开 O(Σm × W):顶格 39.5 亿次跑不完,但题面 30% 那档只有 950 万次 ⇒ 稳拿 30 分 |
| 拆分省了多少 | ★ 175 倍(顶格实测) |
| ★★★ 错法一 / 二 | 拆多了(能拿超过 m 件)/拆少了(余数丢了)—— 方向相反 |
| ★★★ 它们的触发条件 | 同一句话:m + 1 不是 2 的幂;m 全取 2^k−1 那一档两个都是精确的 0 |
| ⚠ 而那只是必要条件 | 默认档 225 轮满足,真被抓只有 81 / 111(差 2.8 倍 / 2.0 倍) |
| 错法三 | 内层正序 ⇒ 恒等于完全背包(300/300),件数上限等于没写 |
| ⚠ 错法四(不在算法里) | 输入是「价值 重量 件数」,读反被抓 268 / 300 |
| ★★ 第 35 章补的那条 | O(nW) 单调队列在顶格上内层少 6.0 倍、秒表快 1.8 倍 —— ⚠ 而两版都只用 5~9 毫秒,这 4 毫秒买不到任何东西 |