题单 · 习题解析

洛谷 P1776 宝物筛选

★★★ 二进制拆分的两个错法(拆多了 / 拆少了)方向相反,触发条件却是同一句话:m + 1 不是 2 的幂 —— 而 m 全取 2^k−1 那一档两个都是精确的 0;★★ 第 35 章补了一节:把转移里的 max 写出来它就是**滑动窗口最大值** ⇒ O(nW),而**在这道题上一分钱不值** —— 顶格内层少 6.0 倍、秒表只快 1.8 倍,而两版都只用 5~9 毫秒(时限 1 秒)⇒ 「这个技巧在这道题上成立」和「这道题该用它」是两句话

原题:洛谷 P1776出自 第 24 章 完全背包与多重背包 的题单出自 第 35 章 单调栈与单调队列 的题单题面本地存档:2026-08-29
⚠ 先自己写一遍,再往下看

这一页不是标准答案,是一条阶梯:把大多数人真实会写出的第一版摆出来, 看它错在哪、慢在哪,再一步一步改。所以它对「已经自己动手撞过一次墙」的人最有用。
还没写过的话,先去写 —— 写出来的哪怕是错的,也比直接看这一页收获大。

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. 拆分错:把「1, 2, 4…, 剩余」写成别的(多拆一个或少拆一个)——两种错法方向相反,却栽在同一句话上,m 凑不齐 0 .. m。
  3. 0/1 背包在二进制拆分上写成正序:件数上限直接失效,变成无界拾取。

能 AC 的:p1776.cpp(二进制拆分 + 0/1 背包内层倒序)这一版能 AC。

下面每一步里灰色的「深入」块是折叠的:那是这些结论怎么实测出来的,第一遍可以跳过。

深入:输入字段顺序(进阶)
⚠⚠ 先说一个和算法完全无关的坑:这道题的输入是「价值 重量 件数」

绝大多数背包题给的是「重量 价值」,而这一道是 vᵢ wᵢ mᵢ —— 价值在前。

随机 300 轮:把两列读反,被抓的轮数 268 / 300
而「某一轮里每种的 v 恰好等于 w」(读反也没关系)的轮数 6

⇒ 它错得很凶(九成轮次),可它照样能通过你随手编的那一两组数据 —— ⚠ 而同一个错法在 P1164 的样例上是恒等变换(那道题的样例是 4 4)。 「样例挡不挡得住」的主语永远是那一组样例长什么样。

1第一版:把 m 件摊开成 m 件独立的物品

第 24 章第 ⑪ 步讲的就是它 —— 最直白,也一定对:

p1776Flat.cpp第一版:摊开成 Σm 件
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

复杂度 O(Σm × W),顶格是 10⁵ × 4 × 10⁴ = 4 × 10⁹ ⇒ 必 TLE。

深入:错法也有档可拿(进阶)
★★ 但它不是零分 —— 题面自己给了它一档

题面写着「对于 30% 的数据,Σmᵢ ≤ 10⁴,W ≤ 10³」。乘出来:

内层次数 本机毫秒
30% 那一档(Σm = 10⁴,W = 10³) 9 509 200 2
100% 顶格(Σm = 10⁵,W = 4 × 10⁴) ★ 3 946 579 000 跑不完

⇒ 「暴力能拿多少分」是一道三十秒的算术题 —— 而不是「反正过不了,不写了」。 ★ 这一条本书量过好几次(P1216、P1115); 这道题特别干净,因为出题人把那一档直接写在数据范围里了。

2★ 正解:二进制拆分

p1776.cpp★ 这一版就能 AC
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

把「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★★★ 拆分的两个错法:方向相反,触发条件却是同一句话

p1776Over.cpp错法一:拆多了(m 没跟着减)
p1776Less.cpp错法二:拆少了(忘了余数那一堆)
    正确    :1, 2, 4, …, 2^(k-1), 余数            加起来正好 m
    拆多了  :1, 2, 4, …, 2^j     (m 没减)        加起来 2^(j+1) − 1  ≥ m   -> 能拿超过 m 件
    拆少了  :1, 2, 4, …(余数丢了)                加起来 < m               -> 有几件永远用不上
深入:两个 bug 栽在同一句话(进阶)
★★★ 两个 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★★ 错法三:内层写成正序 —— 件数上限整个失效

p1776Full.cpp错法三:内层正序
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
随机 300 组:它的答案 vs 显式写的完全背包(每种无限件) ★ 300 / 300 逐组相等
它被抓的轮数 171 / 300

⇒ 拆分那一层写得再对也没用:内层方向一错,件数上限就等于没写。 ★ 这是第 24 章第 ⑧ 步那句话的第三次现场 (P1616 是反过来的:完全背包写成倒序 ⇒ 变回 01 背包)。

5★★ 第 35 章补的一节:O(nW) 的单调队列 —— 而它在这道题上值不值钱要量

深入:章节间还账(进阶)
★ 第 24 章欠的那句话,第 35 章还上了

第 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ᵢ 完全无关。

multiQueue.cpp(第 35 章那份,一个字没改)★ O(nW):它的输入格式和这道题一模一样,直接就能交
深入:更快但值不了钱(进阶)
⚠ 但「它更快」在这道题上一分钱都不值 —— 这是量出来的,不是推的

第 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 步那个「读要在写之前」)。

p1776Cmp.cpp上面那张表的度量程序
深入:摊开这一版真过不去(进阶)
★ 而摊开那一版是真过不去的 —— 这才是这道题挂在第 35 章题单上的理由
顶格 Σm = 10⁵、W = 4×10⁴ 件数 内层量级
摊开成 Σm 件(第 ① 步) 100 000 4 × 10⁹ ⇒ 跑不完
二进制拆分 ★ 1 000(少 100 倍) 2.4 × 10⁷
单调队列 —(不拆) 4 × 10⁶

★ 所以这道题在第 35 章题单里的位置是「O(nW) 的用武之地」—— ⚠ 而实测把话说得更准了:是「用得上」,不是「非用不可」。

6参照物、度量程序和生成器

p1776Brute.cpp参照物:枚举每种拿几件
p1776Swap.cpp错法四:读成 w v m
p1776Count.cpp度量程序(本页所有数字都出自它)
p1776Gen.cpp数据生成器

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 毫秒买不到任何东西