0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1352,日期见页头。两边不一致时信原站。
题目描述
某大学有 n 个职员,编号为 1 … n。
他们之间有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直接上司。
现在有个周年庆宴会,宴会每邀请来一个职员都会增加一定的快乐指数 rᵢ,
但是呢,如果某个职员的直接上司来参加舞会了,那么这个职员就无论如何也不肯来参加舞会了。
所以,请你编程计算,邀请哪些职员可以使快乐指数最大,求最大的快乐指数。
输入格式
输入的第一行是一个整数 n。
第 2 到第 (n + 1) 行,每行一个整数,第 (i + 1) 行的整数表示 i 号职员的快乐指数 rᵢ。
第 (n + 2) 到第 2n 行,每行输入一对整数 l, k,代表 k 是 l 的直接上司。
输出格式
输出一行一个整数代表最大的快乐指数。
说明/提示
数据规模与约定
对于 100% 的数据,保证 1 ≤ n ≤ 6 × 10³,−128 ≤ rᵢ ≤ 127,1 ≤ l, k ≤ n,
且给出的关系一定是一棵树。
输入输出样例
输入
7 1 1 1 1 1 1 1 1 3 2 3 6 4 7 4 4 5 3 5
输出
5
七个人,每个人的快乐指数都是 1。5 号是校长(他从没在左边那一列出现过),
3、4 是他的直接下属,1 2 挂在 3 下面、6 7 挂在 4 下面。
请 5 1 2 6 7 五个人,快乐指数 5。
★ 这一组样例把本页两个错法都挡住了(贪心 3、从 0 号找根 0)。 ⚠ 而它顺带是本书那条规律(「样例是个一测就死的过滤器:挡住每组都错的,放过偶尔才错的」) 的一个反例 —— 「从 0 号找根」确实每组都错,可贪心 300 轮里只错 55 轮,样例照样一测就死。
1★ 第一版:谁的快乐指数高就先请谁
这道题的第一反应几乎是同一个:把所有人按快乐指数从大到小排队,轮着看, 只要他的上司和下属都还没请,就把他请来(负数的当然不请)。
// ✗ P1352 第一版:按快乐指数从大到小,能选就选//// 大多数人第一眼的想法:谁带来的快乐多就先请谁,只要他的上司和下属都还没请。// 它给出的**是一个真能办成的名单**(没有任何一对上下级同时在场)// ⇒ 所以它恒 ≤ 正解,永远不会「多报」。// 错在哪:请了一个大的,可能挡住两个更大的(经典的独立集反例:一个 100 卡住两个 90)。
#include <bits/stdc++.h>using namespace std;
const int MAXN = 6005;int n, r[MAXN];vector<int> adj[MAXN]; // 无向:上司和下属都算相邻bool taken[MAXN];
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
if (!(cin >> n) || n <= 0) return 0; for (int i = 1; i <= n; i++) cin >> r[i]; int l, k; while (cin >> l >> k) { if (l == 0 && k == 0) break; adj[l].push_back(k); adj[k].push_back(l); }
vector<int> ord(n); for (int i = 0; i < n; i++) ord[i] = i + 1; sort(ord.begin(), ord.end(), [](int a, int b) { return r[a] > r[b]; });
long long sum = 0; for (int u : ord) { if (r[u] <= 0) continue; // 负数请来只会更差 bool blocked = false; for (int v : adj[u]) if (taken[v]) { blocked = true; break; } if (blocked) continue; taken[u] = true; sum += r[u]; } cout << sum << '\n'; return 0;}点「运行 ▶」看结果
它排出来的名单是真能办成的(没有任何一对上下级同时在场)⇒ 它恒 ≤ 正解,永远不会多报。这就是第 26 章 P1220 那条判据的又一次: 看你给出的是「一个合法方案」还是「一个放宽了的问题的解」,答案偏大还是偏小不用跑就能判。
| 它 ≤ 正解 | ★ 300 / 300 |
| 300 轮被抓 | 55 |
| 错的时候平均少拿 | 13.30% |
| 最多少拿 | 56.07% |
最小的反例只要三个人:上司 100、两个下属各 90。 贪心先抓住那个 100,于是两个 90 都进不来(拿 100);正解是把上司晾着,请两个下属(拿 180)。
2★★ 正解:状态里多一维「他自己来不来」
f[u][0] = u 不来时,u 这棵子树里最大的快乐指数和
f[u][1] = u 来时,同上
f[u][0] = Σ max(f[v][0], f[v][1]) 下属来不来都行,取大的那个
f[u][1] = r[u] + Σ f[v][0] u 来了,下属一个都不能来答案是 max(f[root][0], f[root][1])。
为什么这一维非有不可:一个人来不来,只影响他的直接下属。 把「他自己来没来」记进状态,父亲那一层就只需要看这两个数, 而不必回头去问整棵子树到底请了谁 —— 这正是本章正文第 ⑥ 步在讲的事。
推导、动画、以及另外五种错法(前序累加、儿子也能来、下属必须来、没找根、忘了取 max) 正文里都有,这一页不重复,只补正文没量过的那几件事。
// P1352 没有上司的舞会 —— 正解:树形 DP,O(n)//// f[u][0] = u 不来时,u 的子树里最大的快乐指数和// f[u][1] = u 来时,同上// f[u][0] = Σ max(f[v][0], f[v][1]) 下属来不来都行,取大的那个// f[u][1] = r[u] + Σ f[v][0] u 来了,下属一个都不能来// 答案 = max(f[root][0], f[root][1])//// ★ 两件和算法无关、却真会挂人的事,都挤在读入这几行里:// ① 根要自己找 —— 输入给的是「l 的上司是 k」,从没当过 l 的那个点才是根;// ② 读到 (0, 0) 就停 —— 题面写的是 n-1 行,而这道题的数据常被人提到// 「末尾多一行 0 0」。挡一句一分钱不吃亏;不挡的写法里有一种会自环。
#include <bits/stdc++.h>using namespace std;
const int MAXN = 6005;int n, r[MAXN], f[MAXN][2];vector<int> son[MAXN];bool hasFa[MAXN];
void dfs(int u) { f[u][0] = 0; f[u][1] = r[u]; for (int v : son[u]) { dfs(v); f[u][0] += max(f[v][0], f[v][1]); // 下属来不来都行 f[u][1] += f[v][0]; // u 来了,下属只能不来 }}
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
if (!(cin >> n) || n <= 0) return 0; for (int i = 1; i <= n; i++) cin >> r[i];
int l, k; while (cin >> l >> k) { if (l == 0 && k == 0) break; // ← ② 挡住那对 (0, 0) son[k].push_back(l); hasFa[l] = true; }
int root = 1; // ← ① 自己找根 for (int i = 1; i <= n; i++) if (!hasFa[i]) { root = i; break; }
dfs(root); cout << max(f[root][0], f[root][1]) << '\n'; return 0;}点「运行 ▶」看结果
3★★★ 「快乐指数有没有负数」这把旋钮,把两个 bug 推向相反方向
正文第 ⑫ 步量过:生成器从「全是正数」改成「−50 ~ 100」之后, 「以为下属必须来」那个 bug 从 195 涨到 268。理由很直白 —— 快乐指数全是正数时,「能来就来」本来就划算,那个错误理解十有八九恰好取到同一个数。
同一把旋钮,对本页这个贪心的作用正好相反:
| 快乐指数 | 全是正数 | −50 ~ 100(有负数) | 全是负数 |
|---|---|---|---|
| 「以为下属必须来」被抓(正文) | 195 / 300 | 268 / 300 | —— |
| 本页的贪心被抓 | 119 / 300 | 55 / 300 | ★ 精确的 0 |
| 「从 0 号找根」被抓(第 ④ 步) | —— | 300 / 300 | ★ 精确的 0 |
⇒ ★★★ 同一个旋钮把两个 bug 推向相反方向 —— 本书量到第五次了 (前四次是第 23 章 P1049、P2925、第 24 章 P5365)—— ⚠ 而这一次两条曲线还分别写在两处:一条在正文,一条在这一页。 「加了负数数据就更强」是没有主语的。
rᵢ 全是负数时,正解恒等于 0(谁都不请)—— 300 / 300 轮如此。
于是「只会少拿」的贪心和「恒输出 0」的错法同时隐身,两个都是结构性的 0,加多少轮都没用。
★ 而 −128 ≤ rᵢ 是题面写着的:全负数据完全合法,只是顺手写的生成器造不出来。
⇒ 又一次「生成器最自然的默认值,正好是某个 bug 的藏身处」。
4★★★ 第二个错法:从 0 号点开始找根 —— 它的来处就在同一张题单里
这道题的根要自己找:输入给的是「l 的上司是 k」,从没在左边那一列出现过的那个点才是根。
正文第 ⑫ 步演示的是「干脆从 1 号点开始 DFS」。这一页换一个更隐蔽的:循环从 0 写起。
// ✗ P1352:把「编号从 0 开始」的习惯带了过来 —— 找根时从 0 号点找起//// ⚠ 这个错法有一个非常具体的来处:**同一章题单里的下一道题 [P2016 战略游戏]// 的结点编号真的是 0 ~ n-1**。先写完那道再回头写这道,很容易顺手写成 `for (i = 0; ...)`。//// 后果:0 号点从来没当过谁的下属 ⇒ 它「没有上司」⇒ 被当成根。// 而 0 号点是个空点(r[0] = 0、没有下属),于是这份程序**恒输出 0**。
#include <bits/stdc++.h>using namespace std;
const int MAXN = 6005;int n, r[MAXN], f[MAXN][2];vector<int> son[MAXN];bool hasFa[MAXN];
void dfs(int u) { f[u][0] = 0; f[u][1] = r[u]; for (int v : son[u]) { dfs(v); f[u][0] += max(f[v][0], f[v][1]); f[u][1] += f[v][0]; }}
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
if (!(cin >> n) || n <= 0) return 0; for (int i = 1; i <= n; i++) cin >> r[i]; int l, k; while (cin >> l >> k) { if (l == 0 && k == 0) break; son[k].push_back(l); hasFa[l] = true; }
int root = 1; for (int i = 0; i <= n; i++) if (!hasFa[i]) { root = i; break; } // ← 从 0 找起
dfs(root); cout << max(f[root][0], f[root][1]) << '\n'; return 0;}点「运行 ▶」看结果
0 号点从来没当过谁的下属 ⇒ 它「没有上司」⇒ 它被当成根。
而 0 号点是个空点(r[0] = 0、没有下属)⇒ 这份程序恒输出 0。
于是:被抓的轮数 ≡ 正解 > 0 的轮数,一个不差(默认档 300 ≡ 300、全负档 0 ≡ 0)。
⚠ 它的来处非常具体:同一章题单的下一道题 P2016 战略游戏,
结点编号真的是 0 ~ n−1。 先写完那道再回头写这道,for (int i = 0; ...) 是顺手就打出来的。
⇒ 第 52 章那条「上一章的正确写法可能就是这一章的 bug」,
这次发生在同一张题单的两道题之间。
5★★ 那句「数据末尾多一行 0 0」—— 量完之后要反过来说
这道题流传很广的一句提醒是:它的输入末尾多一行 0 0。
题面写的是「第 n+2 到第 2n 行」,正好 n−1 行,样例里也没有那一行。
我们没法去查洛谷的测试数据,但能查的是另一半:
真有那一行的话,哪几种读法会挂?没有那一行呢?
| 读法 | 没有 0 0 |
有 0 0 |
|---|---|---|
① 照题面读 n−1 行 |
300 / 300 | 300 / 300 |
| ② 读到 EOF,不挡 | 300 / 300 | 300 / 300 |
| ③ 读到 EOF,不挡 + 找根从 0 写起 | ★ 0 / 300 | 300 / 300 |
④ 读到 EOF,挡住 (0, 0) ← 正解 |
300 / 300 | 300 / 300 |
② 为什么不挡也没事:多读到的 (0, 0) 只会往 son[0] 里塞一条边,
而根在 1 … n 里,DFS 一次也走不到 0 号点。
★ 真正扎人的是 ③,而它恰恰是「照顾那句提醒」照顾出来的 ——
数据里真有 0 0 时它反而对了(hasFa[0] 被置上,0 号点不再像根),
没有那一行时它满盘皆输。
⇒ 结论不是「那句提醒是错的」,是「按它去改代码可能改出 bug」:
写成第 ④ 行那样(while (cin >> l >> k) { if (l == 0 && k == 0) break; ... }),
两种数据下都对,一分钱不吃亏。
6★ 参照物、规模和上界
参照物就是最朴素的 2ⁿ 枚举「谁到场」,逐条边检查有没有上下级同时在场
—— 第 23 章 P1060 那条「顶格对拍的参照物有时就是暴力本身」在这儿用不上
(n ≤ 6000),但小数据对拍它足够了。
300 轮:正解 vs 2ⁿ 枚举 |
★ 不一致 0 轮 |
| 答案恒 ≥ 0(一个都不请) | ★ 300 / 300 |
顶格 n = 6000、−128 ≤ rᵢ ≤ 127 ⇒ 答案上界 |
762 000 ⇒ int 够 |
| 递归深度 | 顶格是一条 6000 层的链 —— 默认栈放得下(⚠ 到了 P3478 的 10⁶ 就放不下了) |
7度量程序和生成器
8一页纸
| ★★ 关键的一步 | f[u][0/1] —— 把「他自己来没来」记进状态(推导见正文) |
| ★ 第一版 | 按快乐指数从大到小能选就选 —— 恒 ≤ 正解,被抓 55/300,错时平均少拿 13.30% |
| ★★★ 一把旋钮,两个方向 | 有没有负数:「下属必须来」195 → 268(更容易抓),贪心 119 → 55(更难抓) |
| ⚠ 全负那一档 | 正解恒为 0 ⇒ 贪心和「从 0 找根」同时变成精确的 0 |
| ★★★ 从 0 号找根 | 恒输出 0 ⇒ 被抓轮数 ≡ 正解 > 0 的轮数(一个不差);来处是同题单的 P2016 |
| ★★ 那句「多一行 0 0」 | 四种读法 × 两种数据,八格里唯一的 0 是照顾它照顾反了的那一格 |
| 参照物 | 2ⁿ 枚举到场名单;300 轮不一致 0 轮 |
| 规模 | n ≤ 6000 ⇒ 答案上界 762 000,int 够;6000 层递归默认栈放得下 |