0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1757,日期见页头。两边不一致时信原站。
题目背景
【管理员备注】本题数据实际满足
aᵢ, bᵢ为非负整数,且0 ≤ cᵢ ≤ 10⁴。 鉴于此题为分组背包经典例题,不会加入aᵢ, bᵢ, cᵢ为负数之类的无意义的 hack 数据。
直达通天路·小 A 历险记第二篇
题目描述
自 01 背包问世之后,小 A 对此深感兴趣。一天,小 A 去远游,却发现他的背包不同于 01 背包,
他的物品大致可分为 k 组,每组中的物品相互冲突,现在,他想知道最大的利用价值是多少。
输入格式
两个数 m, n,表示一共有 n 件物品,背包能承受的最大重量为 m。
接下来 n 行,每行 3 个数 aᵢ, bᵢ, cᵢ,表示物品的重量,利用价值,所属组数。
输出格式
一个数,最大的利用价值。
说明/提示
0 ≤ m ≤ 1000,1 ≤ n ≤ 1000,1 ≤ k ≤ 100,aᵢ, bᵢ, cᵢ 在 int 范围内。
输入输出样例
输入
45 3 10 10 1 10 5 1 50 400 2
输出
10
背包能装 45,三件物品:前两件同属第 1 组(互相冲突,只能选一件),第三件在第 2 组但重 50,装不下。 ⇒ 只能在第 1 组里挑价值大的那件:10。
★ 这一组样例挡住了「第一行读反」(打 0)和「组内提到容量外」(打 15), ⚠ 而对本页的主角 ——「只扫组号 1..100」—— 完全无能为力(样例的组号就是 1 和 2)。
1★ 第一关不是 DP,是那两个数的顺序
题面第一句:「两个数 m, n,表示一共有 n 件物品,背包能承受的最大重量为 m」——
先容量、后件数。而绝大多数背包题是反过来写的(n 在前)。
// ✗ P1757 的第一个坑,和算法一点关系都没有:第一行两个数读反了。//// 题面第一句是「两个数 `m, n`,表示一共有 `n` 件物品,背包能承受的最大重量为 `m`」——// **先容量、后件数**,而绝大多数背包题是反过来的。//// ★ 这一版把它读成了 `n m`。官方样例 `45 3` 于是变成「45 件物品、容量 3」:// 后面只有 3 行可读,读完就没了 ⇒ 打出 0。**样例当场挡住。**// ⚠ 这份代码老老实实检查了 `scanf` 的返回值 —— 读不到就停,// 免得拿一堆未初始化的垃圾去做 DP(那样打出来的数**换个编译器就变**,没法写进正文)。
#include <bits/stdc++.h>using namespace std;
int main() { int m, n; if (scanf("%d %d", &n, &m) != 2) return 0; // ✗ 读反了 map<int, vector<pair<int, int>>> by; for (int i = 0; i < n; i++) { int a, b, c; if (scanf("%d %d %d", &a, &b, &c) != 3) break; // 读不到就停 by[c].push_back({ a, b }); } if (m < 0) m = 0; vector<int> f(m + 1, 0); for (auto& kv : by) for (int j = m; j >= 0; j--) for (auto& it : kv.second) if (it.first <= j) f[j] = max(f[j], f[j - it.first] + it.second); printf("%d\n", f[m]); return 0;}点「运行 ▶」看结果
样例 45 3 于是变成「45 件物品、容量 3」,后面只有 3 行可读 ⇒ 打出 0。样例当场挡住。
这一版老老实实检查了 scanf 的返回值,读不到就停。
如果放任它拿未初始化的局部变量去做 DP,打出来的数换个编译器就变 ——
第 43 章那一列就是这么栽的:靠未定义行为出错的数字原理上不可复现,
写进正文等于写了一句下次跑就不成立的话。
2★ 关键的一步:按组号归类,然后三层循环
第 25 章正文第 11 步那三层循环,一个字都不用改:
for 每一组
for j = m .. 0 ← 容量,倒序
for 组内每一件
f[j] = max(f[j], f[j - a] + b)
这道题唯一多出来的动作是归类:物品是「每件自带组号」发下来的, 而且同一组的物品在输入里不一定连着(生成器专门打乱过)。
// P1757 通天之分组背包 —— ★ 这一版就能 AC。//// 第 25 章下半场那三层循环,原样搬过来:// for 每一组// for j = m .. 0 ← 容量,倒序// for 组内每一件// f[j] = max(f[j], f[j - a] + b)//// ⚠ 这道题真正要过的第一关不是 DP,是**读入**:// ① 第一行是 `m n` —— **先容量、后件数**(读反了会读到一堆垃圾,见 p1757Swap.cpp);// ② 物品是「每件自带组号」,要先按组号归类;// ③ ★★ 而**组号不是 1..k** —— 题面写的是「`1 ≤ k ≤ 100`」(**组数**至多 100),// 管理员备注又写着「`0 ≤ cᵢ ≤ 10⁴`」(**组号**能到一万)。这两句差 100 倍。// ⇒ 这里用一张 `10001` 个桶的表归类,再把**真正出现过**的组号收集起来跑。
#include <bits/stdc++.h>using namespace std;
const int MAXC = 10000;
int main() { int m, n; if (scanf("%d %d", &m, &n) != 2) return 0; // ★ 先 m(容量)后 n(件数) vector<vector<pair<int, int>>> grp(MAXC + 1); // grp[组号] = {(重量, 价值), ...} vector<int> ids; for (int i = 0; i < n; i++) { int a, b, c; if (scanf("%d %d %d", &a, &b, &c) != 3) return 0; if (c < 0 || c > MAXC) continue; if (grp[c].empty()) ids.push_back(c); grp[c].push_back({ a, b }); }
vector<int> f(m + 1, 0); for (int c : ids) for (int j = m; j >= 0; j--) // 容量倒序:读到的 f[j-a] 还没被本组碰过 for (auto& it : grp[c]) // 组内枚举必须在最里层 if (it.first <= j) f[j] = max(f[j], f[j - it.first] + it.second); printf("%d\n", f[m]); return 0;}点「运行 ▶」看结果
3★★★ 题面上那两行数字,一行说的是「有几组」,另一行说的是「组号能有多大」
数据范围那一行写着 1 ≤ k ≤ 100。k 在题目描述里是「物品大致可分为 k 组」——
组数。而管理员备注里另有一句:0 ≤ cᵢ ≤ 10⁴ —— 那是组号的取值范围。
至多 100 个组,组号却可以撒在 0 ~ 10 000 之间。
于是「照着 k ≤ 100 开数组、照着 for (g = 1; g <= 100; g++) 扫一遍」这种写法,
会把组号更大的那些物品整组漏掉:
// ✗ P1757 的第二个坑:照着「`1 ≤ k ≤ 100`」这一行,把组号也当成了 1..100。//// 题面数据范围里那一行写着 `1 ≤ k ≤ 100`,而 `k` 在题目描述里指的是「物品大致可分为 k 组」——// 也就是**组数**。可管理员备注里另写了一句:`0 ≤ cᵢ ≤ 10⁴` —— 那是**组号的取值范围**。//// ★★ 这两句差 **100 倍**,而它们说的是两件事:// 「有几组」是 100,「组号能有多大」是 10 000。//// 这一版只扫组号 1..100,于是组号更大的那些物品**整组被漏掉**(答案偏小)。
#include <bits/stdc++.h>using namespace std;
int main() { int m, n; if (scanf("%d %d", &m, &n) != 2) return 0; vector<vector<pair<int, int>>> grp(101); for (int i = 0; i < n; i++) { int a, b, c; if (scanf("%d %d %d", &a, &b, &c) != 3) return 0; if (c >= 1 && c <= 100) grp[c].push_back({ a, b }); // ✗ 组号 > 100 的全丢了 } vector<int> f(m + 1, 0); for (int c = 1; c <= 100; c++) for (int j = m; j >= 0; j--) for (auto& it : grp[c]) if (it.first <= j) f[j] = max(f[j], f[j - it.first] + it.second); printf("%d\n", f[m]); return 0;}点「运行 ▶」看结果
样例上它打出 10,一点破绽都没有 —— 因为样例的组号就是 1 和 2。
只拧一个开关:组号怎么发(各 300 轮,别的一个字不改)
| 组号怎么发 | 「只扫 1..100」被抓 | 而「真有组号 > 100」的轮数 |
|---|---|---|
顺手编成 1, 2, 3, …(几乎所有人的第一版生成器) |
★ 精确的 0 | 0 |
照题面撒在 [0, 10⁴] |
268 / 300 | 300 / 300 |
★ 上面那个 0 不是概率低,是结构性的:组号根本没机会超过 100。 ⇒ 又一次「两种 0,造两档就能分开」,而这一次造出那一档几乎不花钱 —— 生成器里发组号那一行改一改而已。
⚠ 值得记住的是默认值的方向:写生成器时「组号就编 1, 2, 3」是最自然的动作, 而它正好把题面明写的那个取值范围抹掉了。 ⇒ 第 22 章 P1020 那条规矩的另一种形态:生成器要照抄题面写的东西, 而不是照抄你以为的样子。
4★ 组内那层循环提到外面 —— 它精确地在解另一道题
// ✗ P1757 的第三个坑:组内那层循环提到了容量循环**外面**。//// 第 25 章正文第 11 步那张表:三层循环的顺序**决定你解的是哪一道题**。// 组 → 组内 → 容量倒序,读到的 `f[j-a]` 是「本组已经放过一件」的值 ⇒ 同一组能拿好几件。//// ★ 它不是「随便错」:它精确地等于**无视分组的 01 背包**(p1757Ignore.cpp),300 轮逐组相等。
#include <bits/stdc++.h>using namespace std;
int main() { int m, n; if (scanf("%d %d", &m, &n) != 2) return 0; map<int, vector<pair<int, int>>> by; for (int i = 0; i < n; i++) { int a, b, c; if (scanf("%d %d %d", &a, &b, &c) != 3) return 0; by[c].push_back({ a, b }); } vector<int> f(m + 1, 0); for (auto& kv : by) for (auto& it : kv.second) // ✗ 组内提到了外面 for (int j = m; j >= it.first; j--) f[j] = max(f[j], f[j - it.first] + it.second); printf("%d\n", f[m]); return 0;}点「运行 ▶」看结果
样例上它打出 15(把同组两件一起拿走了),样例挡住。
| 300 轮:它 vs 老老实实的 01 背包 | ★ 300 / 300 逐组相等 |
| 默认档里它被抓 | 142 / 300 |
⇒ 第 25 章正文第 11 步那张表在真题上的复现: 循环顺序不是「格式」,它就是题目本身。
5⚠⚠ 那三层循环也有一个没写出来的前提:重量 ≥ 1
组内那层在最里面,读的是 f[j-a]。一旦 a = 0,读到的就是同一格 ——
而那一格可能已经被本组的前一件改过了。于是同一组会被拿走好几件。
⚠ 而管理员备注写的是「aᵢ, bᵢ 为非负整数」—— 0 是允许的。
| 生成器 | 标准三层循环 vs 暴力 | 稳的写法 vs 暴力 | 而「真有重量为 0 的物品」 |
|---|---|---|---|
照顺手的写法随机(a ≥ 1) |
0 | 0 | 0 |
| 允许重量为 0 | ★ 37 / 300 不一致 | ★ 0 | 110 / 300 |
★ 治法只有一行:组内枚举时统一读「这一组还没动手之前」的那份 f
(每组多拷一份,O(组数 × m) = 100 × 1000,随便)。
⇒ 这是本章前一页 P1855 那件事的第二次现场:
那一页是「二维费用的内层循环怎么写都一样 —— 前提是 m ≥ 1」,
这一页是「组内枚举放最里层就对 —— 前提是 a ≥ 1」。
★★ 同一个前提在同一章里咬了两次,而两次都写在题面没管的那个下界上。
⚠ 说清楚证据等级:这不是说交上去会 WA —— 洛谷那份数据里显然没有这种输入(否则全网的模板写法都过不了)。 量它是为了记住那个前提存在,以及生成器该往哪儿拧。
6★ 规模:一道三十秒的算术题
每件物品被内层扫一遍容量 ⇒ 顶格 n × m |
1000 × 1000 = 100 万次 |
| 按组号归类要多少个桶 | 10 001 个(组号 ≤ 10⁴,不是 100) |
f 数组 |
3 KB |
⇒ 这道题的复杂度和「有几组」无关 —— 每件物品只会被它自己那一组扫到一次。
7度量程序、生成器和参照物
8一页纸
| 第一关 | 第一行是 m n(先容量后件数)—— 读反了样例当场打 0 |
| 关键的一步 | 按组号归类(同组的物品在输入里不一定连着),然后组 → 容量倒序 → 组内 |
| ★★★ 题面那两行数字 | 1 ≤ k ≤ 100 是组数,0 ≤ cᵢ ≤ 10⁴ 是组号 —— 差 100 倍,说的不是一件事 |
| 那个精确的 0 | 生成器把组号编成 1..k ⇒「只扫 1..100」被抓 0 / 300;照题面撒 ⇒ 268 / 300 |
| 组内提到外面 | ≡ 无视分组的 01 背包(300/300 逐组相等),默认档被抓 142 |
| ⚠⚠ 那三层循环的前提 | a ≥ 1。允许重量为 0 之后,标准写法和暴力 37 / 300 不一致(稳的写法 0) |
| ★★ 同一章第二次 | P1855 是「内层方向无所谓 —— 前提 m ≥ 1」,这里是「组内放最里层 —— 前提 a ≥ 1」 |
| 规模 | 顶格 1000 × 1000 = 100 万次;桶表 10 001 个;f 数组 3 KB |