0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1064,日期见页头。两边不一致时信原站。
题目描述
金明今天很开心,家里购置的新房就要领钥匙了,新房里有一间金明自己专用的很宽敞的房间。
更让他高兴的是,妈妈昨天对他说:「你的房间需要购买哪些物品,怎么布置,你说了算,
只要不超过 n 元钱就行」。今天一早,金明就开始做预算了,他把想买的物品分为两类:
主件与附件,附件是从属于某个主件的,下表就是一些主件与附件的例子:
| 主件 | 附件 |
|---|---|
| 电脑 | 打印机,扫描仪 |
| 书柜 | 图书 |
| 书桌 | 台灯,文具 |
| 工作椅 | 无 |
如果要买归类为附件的物品,必须先买该附件所属的主件。
每个主件可以有 0 个、1 个或 2 个附件。 每个附件对应一个主件,附件不再有从属于自己的附件。
金明想买的东西很多,肯定会超过妈妈限定的 n 元。于是,他把每件物品规定了一个重要度,
分为 5 等:用整数 1 ~ 5 表示,第 5 等最重要。
他还从因特网上查到了每件物品的价格(都是 10 元的整数倍)。
他希望在不超过 n 元的前提下,使每件物品的价格与重要度的乘积的总和最大。
设第 j 件物品的价格为 vⱼ,重要度为 wⱼ,共选中了 k 件物品,编号依次为 j₁, j₂, …, j_k,
则所求的总和为:
v(j₁) × w(j₁) + v(j₂) × w(j₂) + … + v(j_k) × w(j_k)
请你帮助金明设计一个满足要求的购物单。
输入格式
第一行有两个整数,分别表示总钱数 n 和希望购买的物品个数 m。
第 2 到第 (m + 1) 行,每行三个整数,第 (i + 1) 行的整数 vᵢ,wᵢ,qᵢ
分别表示第 i 件物品的价格、重要度以及它对应的的主件。如果 qᵢ = 0,表示该物品本身是主件。
输出格式
输出一行一个整数表示答案。
说明/提示
对于全部的测试点,保证 1 ≤ n ≤ 3.2 × 10⁴,1 ≤ m ≤ 60,0 ≤ vᵢ ≤ 10⁴,
1 ≤ wᵢ ≤ 5,0 ≤ qᵢ ≤ m,答案不超过 2 × 10⁵。
NOIP 2006 提高组 第二题
输入输出样例
输入
1000 5 800 2 0 400 5 1 300 5 1 400 3 0 500 2 0
输出
2200
1000 元,5 件物品:第 1 件是主件(800 元,重要度 2),第 2、3 件是它的附件;第 4、5 件也是主件。
- 买第 1 件 + 它的附件?
800 + 400 = 1200元,超了;只买第 1 件是800 × 2 = 1600。 - 买第 4 件 + 第 5 件:
400 + 500 = 900元,400 × 3 + 500 × 2 =2200。 ✓
★ 这一组样例挡住了「全当独立物品」(打 3500), ⚠ 而对「一边读一边归组」完全无能为力 —— 因为样例里主件正好都排在附件前面。
1第一版:把「附件」当背景,直接做 01 背包
题面前半段全在讲家具,「价格 × 重要度求和最大」看上去就是一道裸的 01 背包。
// ✗ P1064 的第一版:把「附件」这两个字当成背景,全当独立物品做一遍 01 背包。//// 这是绝大多数人真实的第一反应 —— 题面前半段全在讲家具,// 「价格 × 重要度求和最大」看上去就是一道裸的 01 背包。//// ⚠ 它丢掉的正是这道题唯一的新东西:**买附件之前必须先买它的主件**。// ★ 它算出来的答案恒 ≥ 正解(可行集变大了),而且官方样例一测就死(3500 vs 2200)。
#include <bits/stdc++.h>using namespace std;
int main() { int n, m; if (scanf("%d %d", &n, &m) != 2) return 0; vector<int> f(n / 10 + 1, 0); for (int i = 1; i <= m; i++) { int v, w, q; if (scanf("%d %d %d", &v, &w, &q) != 3) return 0; (void)q; // ✗ 主件是谁?不管 for (int j = n / 10; j >= v / 10; j--) f[j] = max(f[j], f[j - v / 10] + v * w); } printf("%d\n", f[n / 10]); return 0;}点「运行 ▶」看结果
样例上它打出 3500 —— 它买了两个附件却没买主件。样例一测就死。
| 300 轮里「它的答案 ≥ 正解」 | ★ 300 / 300 |
| 默认档里它被抓 | 92 / 300 |
⚠ 注意后一行:它只在 92 / 300 轮里露馅 —— 剩下那 208 轮, 「不买主件也能买附件」这件事恰好没被最优解用上。 ⇒ 「样例一测就死」和「对拍常常抓不到」在同一个 bug 上并不矛盾。
2★★★ 关键的一步:它是分组背包,只是没写在脸上
① 只买主件
② 主件 + 附件1
③ 主件 + 附件2
④ 主件 + 两个附件这四种买法互相冲突(一个主件只买一次),而「一组里至多挑一件」正是分组背包。
★ 「组内为什么最多就是 4 种」——题面写死的:「每个主件可以有 0 个、1 个或 2 个附件」。
组内选法数是 2^附件数,附件上限 2 ⇒ 4 种。⚠ 上限如果是 10,组内就是 1024 种。
⇒ 这句话是第 12 章那三类约束里的情报:它直接告诉你「组内枚举花得起」。
// P1064 [NOIP 2006 提高组] 金明的预算方案 —— ★ 这一版就能 AC。//// ★ 关键的一步:**它是分组背包,只是没写在脸上。**// 一个主件和它的附件绑成**一组**,组内是四种互斥的买法:// ① 只买主件 ② 主件 + 附件1 ③ 主件 + 附件2 ④ 主件 + 两个附件// (题面写死了「每个主件可以有 0 个、1 个或 2 个附件」⇒ 组内最多就是这 4 种。)// 然后就是第 25 章那三层循环:组 → 容量倒序 → 组内枚举。//// ⚠ 读入有一处不能想当然:题面**没有保证附件排在它的主件后面**,// 所以要先把 m 件全读进来,再分两趟归组(见 p1064OnePass.cpp 那个反例)。//// ★ 「价格都是 10 元的整数倍」是题面发的一张许可证:钱数除以 10,规模缩 10 倍。
#include <bits/stdc++.h>using namespace std;
int main() { int n, m; if (scanf("%d %d", &n, &m) != 2) return 0; vector<int> v(m + 1), w(m + 1), q(m + 1); for (int i = 1; i <= m; i++) if (scanf("%d %d %d", &v[i], &w[i], &q[i]) != 3) return 0;
n /= 10; // ★ 许可证:价格都是 10 的倍数 vector<vector<int>> att(m + 1); // att[主件] = 它的附件编号 for (int i = 1; i <= m; i++) if (q[i] != 0) att[q[i]].push_back(i);
// 每个主件一组,组内枚举「带哪几个附件」(0 ~ 2 个 ⇒ 最多 4 种) vector<int> f(n + 1, 0); for (int i = 1; i <= m; i++) { if (q[i] != 0) continue; // 附件不单独成组 int cnt = (int)att[i].size(); for (int j = n; j >= 0; j--) for (int s = 0; s < (1 << cnt); s++) { // 组内那层,必须在最里面 int cost = v[i] / 10, val = v[i] * w[i]; for (int t = 0; t < cnt; t++) if (s >> t & 1) { int k = att[i][t]; cost += v[k] / 10; val += v[k] * w[k]; } if (cost <= j) f[j] = max(f[j], f[j - cost] + val); } } printf("%d\n", f[n]); return 0;}点「运行 ▶」看结果
3★★★ 一个和算法无关、样例也抓不到的坑:附件不一定排在主件后面
最顺手的写法是一边读一边归组:读到 q = 0 就新开一组,读到 q > 0 就挂到第 q 组上。
// ✗ P1064 的第二版:一边读一边归组 —— 假设了「附件一定排在它的主件后面」。//// 读到 `q = 0` 就新开一组,读到 `q > 0` 就直接挂到「第 q 件所在的那一组」上。// 只要输入是「主件在前」,它就一点毛病都没有 —— 官方样例正好就是这样,所以样例放过它。//// ⚠ 而题面**从来没保证过这个顺序**(只说了 `qᵢ` 是它对应的主件,`0 ≤ qᵢ ≤ m`)。// ★ 页面上量过:把输入的行序打乱,它当场就现形。// (这一版遇到「主件还没出现」的附件时直接把它丢掉 —— 行为是确定的,// 不去读没初始化的东西,免得打出一个换台机器就变的数。)
#include <bits/stdc++.h>using namespace std;
int main() { int n, m; if (scanf("%d %d", &n, &m) != 2) return 0; vector<int> v(m + 1), w(m + 1); vector<vector<int>> att(m + 1); vector<int> mains; vector<char> seen(m + 1, 0); for (int i = 1; i <= m; i++) { int q; if (scanf("%d %d %d", &v[i], &w[i], &q) != 3) return 0; if (q == 0) { mains.push_back(i); seen[i] = 1; } else if (seen[q]) att[q].push_back(i); // ✗ 主件还没读到?那就当这件不存在 }
int cap = n / 10; vector<int> f(cap + 1, 0); for (int i : mains) { int cnt = (int)att[i].size(); for (int j = cap; j >= 0; j--) for (int s = 0; s < (1 << cnt); s++) { int cost = v[i] / 10, val = v[i] * w[i]; for (int t = 0; t < cnt; t++) if (s >> t & 1) { int k = att[i][t]; cost += v[k] / 10; val += v[k] * w[k]; } if (cost <= j) f[j] = max(f[j], f[j - cost] + val); } } printf("%d\n", f[cap]); return 0;}点「运行 ▶」看结果
样例上它打出 2200,和正解一样。
而题面从来没保证过这个顺序 —— 它只说了「qᵢ 表示它对应的主件」「0 ≤ qᵢ ≤ m」。
| 生成器 | 「一趟读」被抓 | 而「真有附件排在主件前面」的轮数 |
|---|---|---|
| 主件在前(和官方样例一样) | ★ 精确的 0 | 0 |
| 打乱行序 | 93 / 300 | 194 / 300 |
★ 上面那个 0 又是结构性的:那种输入根本造不出来。 ⚠ 而默认值的方向再一次值得记住:写生成器时「先写主件再写它的附件」是最自然的动作 —— 它和官方样例一样,正好把这个 bug 藏起来。 ⇒ 这已经是本章第二次了(P1757 是「组号顺手编成 1..k」): ★★ 生成器最自然的那个默认值,往往正是某个 bug 的藏身处。
★ 顺带又是一次「触发条件 ≠ 抓获数」:194 轮里真被抓 93 轮(差 2.1 倍)—— 附件排在前面只是第一层,那件附件还得真的落在最优解上。
4★ 「价格都是 10 元的整数倍」是一张许可证 —— ⚠ 但这一次不用它也过得去
| 除以 10 | 不除 | |
|---|---|---|
| DP 内层执行次数 | 192 060 | 1 920 060(★ 9.99 倍) |
f 数组 |
12 KB | 125 KB |
| 两版答案 | 相同 | 随机 300 / 300 轮逐组相等 |
⇒ ★ 两列都在限制之内(这道题的空间限制是 512 MB、时限 1 秒)—— 所以这张许可证在这道题上是「用了更漂亮」,不是「不用就挂」。
⚠ 别和 P1853 搞混:那道题的许可证是 a 为 1000 的倍数,
不用它就是 172 MB,先 MLE 再 TLE。
⇒ 同一句话(「所有金额都是某个数的倍数」)在两道题上的分量完全不同,只能自己乘一遍。
5★★ 题面那句「答案不超过 2 × 10⁵」有多紧
本书量过好几句这种「保证」,松紧差得远。这一句可以两行算完:
买下的总价 ≤ n = 32 000,而每一元最多贡献 5(重要度 ≤ 5)
⇒ 答案 ≤ 5 × 32 000 = 160 000
| 算出来的真实上界 | 160 000 |
而它可达吗(造一件 v = 32000, w = 5) |
★ 可达,实测正好 160 000 |
| 题面保证 | 200 000 ⇒ 只松 1.25 倍 |
int 的余量 |
10 737 倍 ⇒ 这道题不需要 long long |
⇒ 把三句放一起看:P1164 恰好够、P1060 松了 666 倍、 这道题 松 1.25 倍。★ 每一句「保证」都得自己乘一遍。
6度量程序、生成器和参照物
7一页纸
| ★★★ 关键的一步 | 它是分组背包:主件 + 它的附件绑成一组,组内四种互斥买法 |
| 组内为什么只有 4 种 | 题面写死「每个主件最多 2 个附件」⇒ 2² = 4(情报:它告诉你组内枚举花得起) |
| 第一版:全当独立物品 | 恒 ≥ 正解(300/300);样例一测就死,⚠ 而对拍只抓 92 / 300 |
| ★★★ 一趟读的坑 | 题面没保证附件排在主件后面;主件在前那一档是精确的 0,打乱行序 93 / 300 |
| ★★ 生成器的默认值 | 「先写主件再写附件」和「组号编成 1..k」(P1757)—— 同一章两次,最自然的默认值就是藏身处 |
| ★ 「价格是 10 的倍数」 | 内层 192 060 → 1 920 060(9.99 倍)、f 12 KB → 125 KB ——⚠ 两列都在限制内,不用也能过 |
| ★★ 「答案 ≤ 2 × 10⁵」 | 真实上界 5 × 32000 = 160 000(可达)⇒ 只松 1.25 倍;int 余量 10 737 倍 |