题单 · 习题解析

洛谷 P1776 宝物筛选

★★★ 二进制拆分的两个错法(拆多了 / 拆少了)方向相反,触发条件却是同一句话:m + 1 不是 2 的幂 —— 而 m 全取 2^k−1 那一档两个都是精确的 0

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

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

0题目原文(本地存了一份)

为什么要在这儿抄一份题面

原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。

转录自洛谷 P1776,日期见页头。两边不一致时信原站。

题目描述

终于,破解了千年的难题。小 FF 找到了王室的宝物室,里面堆满了无数价值连城的宝物。

这下小 FF 可发财了,嘎嘎。但是这里的宝物实在是太多了,小 FF 的采集车似乎装不下那么多宝物。 看来小 FF 只能含泪舍弃其中的一部分宝物了。

小 FF 对洞穴里的宝物进行了整理,他发现每样宝物都有一件或者多件。他粗略估算了下每样宝物的价值, 之后开始了宝物筛选工作:小 FF 有一个最大载重为 W 的采集车,洞穴里总共有 n 种宝物, 每种宝物的价值为 vᵢ,重量为 wᵢ每种宝物有 mᵢ。 小 FF 希望在采集车不超载的前提下,选择一些宝物装进采集车,使得它们的价值和最大。

输入格式

第一行为两个整数 nW,分别表示宝物种数和采集车的最大载重。

接下来 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 ≤ 1001 ≤ 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)。

⚠⚠ 先说一个和算法完全无关的坑:这道题的输入是「价值 重量 件数」

绝大多数背包题给的是「重量 价值」,而这一道是 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 跑不完

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

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 一个多算一个少算,而它们栽在同一句话上

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参照物、度量程序和生成器

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

6一页纸

关键的一步 二进制拆分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