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)。
绝大多数背包题给的是「重量 价值」,而这一道是 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。
题面写着「对于 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 宝物筛选 —— ★ 这一版就能 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 -> 有几件永远用不上
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参照物、度量程序和生成器
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 |