0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1395,日期见页头。两边不一致时信原站。
题目描述
有一个村庄居住着 n 个村民,有 n−1 条路径使得这 n 个村民的家连通,每条路径的长度都为 1。
现在村长希望在某个村民家中召开一场会议,村长希望所有村民到会议地点的距离之和最小,
那么村长应该要把会议地点设置在哪个村民的家中,并且这个距离总和最小是多少?
若有多个节点都满足条件,则选择节点编号最小的那个点。
输入格式
第一行,一个数 n,表示有 n 个村民。
接下来 n−1 行,每行两个数字 a 和 b,表示村民 a 的家和村民 b 的家之间存在一条路径。
输出格式
一行输出两个数字 x 和 y。x 表示村长将会在哪个村民家中举办会议,y 表示距离之和的最小值。
数据范围
- 对于 70% 的数据,
n ≤ 10³; - 对于 100% 的数据,
n ≤ 5×10⁴。
时限 1 秒,内存 128000 KB(125 MiB)。
输入输出样例
输入
4 1 2 2 3 3 4
输出
2 4
⚠ 两件事值得先看一眼:① 洛谷这组样例的每一行边末尾都带一个空格(本地这份也照抄了);
② 这四个点是一条链 1−2−3−4,而 2 号和 3 号的距离之和都是 4 ——
官方唯一这组样例,本身就是一组平局。
1★★★ 这道题的全部功课:先问一句「要不要 LCA」
第 51 章题单里这道题的注解自己写着: 「入门难度的树上题,不需要 LCA。★ 放在这儿是想说明:不是每道树上题都要上倍增。」
它问的是「某个点到所有点的距离和」f[u],而这个量在父子之间只差一句话:
f[v] = f[u] + n − 2·size[v] (u 是 v 的父亲)会议地点从 u 挪到 v:v 那棵子树里的 size[v] 个人各近了一步,
剩下的 n − size[v] 个人各远了一步 ⇒ 净变化 (n − size[v]) − size[v]。
⇒ 一趟 BFS 求出 size[] 和 f[1],再顺着 BFS 序把 f 推下去,O(n) 就完了。
和 P2420 一样,这一页也是「先想清楚这题到底要不要那个大家伙」。
// P1395 会议 —— 正解:换根 DP。**连 LCA 都不用,两趟遍历就完**//// ★★★ 这道题挂在[第 51 章](/ch/51-lca/)的题单里,理由题单注解自己写着:// 「不是每道树上题都要上倍增」。//// ★ 它问的是「某个点到**所有**点的距离和」,而那个量在**父子之间只差一句话**://// f[v] = f[u] + n − 2·size[v] (u 是 v 的父亲)//// —— 会议地点从 u 挪到 v:v 那棵子树里的 size[v] 个人各近了 1 步,// 剩下的 n − size[v] 个人各远了 1 步 ⇒ 净变化 (n − size[v]) − size[v]。//// ⇒ 一趟 BFS 求出 size[] 和 f[1],再顺着 BFS 序把 f 推下去,**O(n)**。//// ⚠ 两处要自己乘一遍:// ① **距离和最大多少**:n = 5×10⁴ 的链,站在一端是 1+2+…+(n−1) = 1 249 975 000// —— int **恰好装得下**(余量 1.72 倍)。这里照样写 long long,代价是零。// ② **平局怎么办**:题面写着「选择节点编号最小的那个点」⇒ 扫的时候只能用**严格小于**。
#include <bits/stdc++.h>using namespace std;
const int MAXN = 50005;int head_[MAXN], nxt_[MAXN * 2], to_[MAXN * 2], ecnt;int fa[MAXN], sz[MAXN], order_[MAXN], n;long long f[MAXN];
inline void addEdge(int u, int v) { to_[++ecnt] = v; nxt_[ecnt] = head_[u]; head_[u] = ecnt; }
int main() { if (scanf("%d", &n) != 1) return 0; for (int i = 0; i < n - 1; i++) { int u, v; if (scanf("%d %d", &u, &v) != 2) return 0; addEdge(u, v); addEdge(v, u); // ⚠ 无向边,两边都要存 }
/* ① BFS 一趟:拿到 BFS 序、父亲、深度和(⚠ 别用递归,链有 5 万层)*/ { vector<char> vis(n + 1, 0); int cnt = 0; order_[cnt++] = 1; vis[1] = 1; fa[1] = 0; long long depSum = 0; vector<int> dep(n + 1, 0); for (int i = 0; i < cnt; i++) { int u = order_[i]; depSum += dep[u]; for (int e = head_[u]; e; e = nxt_[e]) { int v = to_[e]; if (vis[v]) continue; vis[v] = 1; fa[v] = u; dep[v] = dep[u] + 1; order_[cnt++] = v; } } f[1] = depSum; // 站在 1 号点的距离和 }
/* ② 逆着 BFS 序求子树大小 */ for (int i = 1; i <= n; i++) sz[i] = 1; for (int i = n - 1; i >= 1; i--) sz[fa[order_[i]]] += sz[order_[i]];
/* ③ 顺着 BFS 序把 f 推下去 —— 一句话的事 */ for (int i = 1; i < n; i++) { int v = order_[i]; f[v] = f[fa[v]] + n - 2LL * sz[v]; }
int best = 1; for (int i = 2; i <= n; i++) if (f[i] < f[best]) best = i; // ⚠ 严格小于:平局时留住编号小的那个 printf("%d %lld\n", best, f[best]); return 0;}点「运行 ▶」看结果
2⚠ 动笔前的两句算术(这道题只需要这两句)
| 要问的 | 乘一遍 | 结论 |
|---|---|---|
| 距离和最大多少 | 顶格 n = 5×10⁴ 的链,站在一端是 1+2+…+(n−1) = 1 249 975 000 |
★ int 恰好够,余量 1.72 倍 |
| 递归会不会爆栈 | 顶格链的深度是 49 999 层,而第 29 章量的门槛是 17.4 万层 | ★ 够,余量 3.5 倍 |
⇒ 两个「恰好够」。⚠ 而正解照样写了 long long、照样用 BFS 不用递归 ——
因为这两样在这道题上代价是零,而余量 1.72 倍是「没有下一次机会」的那种余量。
★ 这和第 32 章那三道题、第 42 章 P1082 是同一个动作: 「要不要 long long」从来不是一个习惯,是一道乘一遍的算术题。
3第一版:每个点各跑一次 BFS
顶格 n = 5×10⁴ |
最大深度 | ✗ 暴力访问多少条边 | ✗ 暴力耗时 | ★ 正解 |
|---|---|---|---|---|
star |
1 | 4 999 900 000 | 8.11 秒 | 0.00 |
binary |
15 | 4 999 900 000 | 9.08 秒 | 0.00 |
chain |
49 999 | 4 999 900 000 | 15.45 秒 | 0.00 |
rand |
25 | 4 999 900 000 | ⚠ 39.53 秒 | 0.00 |
★★★ 暴力那一列的次数是能算死的:每跑一次 BFS 都要把 2(n−1) 条有向边正好走一遍,
跑 n 次就是 n × 2(n−1) —— 和树长什么样一点关系都没有。
可秒表在同一批数据上差了 4.9 倍。
⇒ 又一次两把尺子打架,⚠ 而这一次连「谁做的活多」都不用争 ——
两边做的活一模一样,差的只是那些活离得多远:
star 上 1 号点的邻居表是一整段连着的,rand 上每一步都在 5 万个点里乱跳。
★ 而这道题上暴力不冤:题面 70% 的数据 n ≤ 10³ ⇒ 只有 200 万次边访问,
本机 0.00 秒 —— 它稳拿 70 分。
4⚠ 四个错法:一个在题面最后一句上,两个在那句公式上,一个在存图上
size[v] ≤ n ⇒ f[u] + n − size[v] ≥ f[u] ⇒ 它推出来的 f 沿着树往下只增不减
⇒ 最小值永远落在 1 号点上 ⇒ 它输出的恒是 1 f[1]。
⇒ 于是「它什么时候会被抓」是一句白送的推论:当且仅当正解的答案不是 1 号点。 ★ 而这句话在四个档上一个不差(第 ⑥ 步那张表:168 ≡ 168、168 ≡ 168、300 ≡ 300、167 ≡ 167)。
⚠ 反过来说,在随机小树上「1 号点恰好就是答案」的比例高达 44% —— 那正是这个错法能骗过一半对拍轮次的原因。
只 addEdge(u, v) 不 addEdge(v, u),BFS 就只能顺着输入给的方向走。
可顺手写的生成器是这样造边的:
for (int i = 2; i <= n; i++) printf("%d %d\n", fa[i], i);—— 每条边都是「父亲写在前」,而 BFS 恰好就是从 1 号点往外走
⇒ 那一档它是结构性的精确的 0。
★ 加一句 if (rng() & 1) swap(a, b); 同一列当场 300 / 300。
⇒ ★★ 这和同一张题单里的 P2420 一模一样 —— 同一个生成器习惯,在这一章里连着藏了两道题的同一个 bug。
5★ 对拍:四个错法 + 一份「一律输出 1 0」
// P1395 的两笔账:这棵树长什么样,以及两条路各做多少次基本动作(机器无关)//// 用法:./p1395Count <csv|table> < 一份输入//// ★ 顺手数一个关键的东西:**有几个点同时取到最小值**(= 平局的规模)——// 那正是错法①(`<` 写成 `<=`)的触发条件。#include <bits/stdc++.h>using namespace std;
const int MAXN = 50005;static int head_[MAXN], nxt_[MAXN * 2], to_[MAXN * 2], ecnt;static int fa[MAXN], sz[MAXN], order_[MAXN], dep[MAXN], n;static long long f[MAXN];
int main(int argc, char** argv) { string mode = argc > 1 ? argv[1] : "table"; if (scanf("%d", &n) != 1) return 0; for (int i = 0; i < n - 1; i++) { int u, v; if (scanf("%d %d", &u, &v) != 2) return 0; to_[++ecnt] = v; nxt_[ecnt] = head_[u]; head_[u] = ecnt; to_[++ecnt] = u; nxt_[ecnt] = head_[v]; head_[v] = ecnt; } vector<char> vis(n + 1, 0); int cnt = 0; order_[cnt++] = 1; vis[1] = 1; long long depSum = 0; for (int i = 0; i < cnt; i++) { int u = order_[i]; depSum += dep[u]; for (int e = head_[u]; e; e = nxt_[e]) { int v = to_[e]; if (vis[v]) continue; vis[v] = 1; fa[v] = u; dep[v] = dep[u] + 1; order_[cnt++] = v; } } for (int i = 1; i <= n; i++) sz[i] = 1; for (int i = n - 1; i >= 1; i--) sz[fa[order_[i]]] += sz[order_[i]]; f[1] = depSum; for (int i = 1; i < n; i++) { int v = order_[i]; f[v] = f[fa[v]] + n - 2LL * sz[v]; }
int maxDep = 0, best = 1, ties = 0; for (int i = 1; i <= n; i++) maxDep = max(maxDep, dep[i]); for (int i = 2; i <= n; i++) if (f[i] < f[best]) best = i; for (int i = 1; i <= n; i++) if (f[i] == f[best]) ties++;
long long worst = 0; // 站在最差的那个点上是多少(顺手看一眼 int 够不够) for (int i = 1; i <= n; i++) worst = max(worst, f[i]);
/* 两条路的基本动作:暴力每个点扫一遍所有边;正解只走两趟 */ long long bruteEdges = (long long)n * (2LL * (n - 1)); long long fastSteps = 2LL * n;
vector<pair<string, string> > out; out.push_back(make_pair("n", to_string(n))); out.push_back(make_pair("maxdep", to_string(maxDep))); out.push_back(make_pair("best", to_string(best))); out.push_back(make_pair("ans", to_string(f[best]))); out.push_back(make_pair("worst", to_string(worst))); out.push_back(make_pair("ties", to_string(ties))); out.push_back(make_pair("brute_edges", to_string(bruteEdges))); out.push_back(make_pair("fast_steps", to_string(fastSteps))); out.push_back(make_pair("ratio", to_string(bruteEdges / fastSteps))); if (mode == "csv") for (size_t i = 0; i < out.size(); i++) printf("%s,%s\n", out[i].first.c_str(), out[i].second.c_str()); else for (size_t i = 0; i < out.size(); i++) printf(" %-12s %s\n", out[i].first.c_str(), out[i].second.c_str()); return 0;}点「运行 ▶」看结果
| 档位 | ①Max | ②Sign | ③Half | ④One | 试金石 |
|---|---|---|---|---|---|
0 顺手写法(随机树、边按 (fa[i], i) 给、编号不打乱) |
39 | 300 | 168 | ★ 0 | 300 |
| 1 + 边随机方向 + 编号打乱 | 39 | 300 | 168 | ★ 300 | 300 |
2 一条链、而且 n 取偶数 |
★ 300 | 300 | 300 | ⚠ 0 | 300 |
| 3 最终档 = 1 + 2 + 形状随机 | 111 | 300 | 167 | 300 | 300 |
★★★ 这张表上的八格「触发 ≡ 抓获」全部命中,而且两条触发条件都写得出来:
| 触发条件 | 四档:满足条件 / 真被抓 | |
|---|---|---|
| ①Max | 这棵树上存在平局(⟺ 有一条边把它劈成点数相同的两半 ⇒ n 必须是偶数) |
39≡39 / 39≡39 / 300≡300 / 111≡111 |
| ③Half | 正解的答案不是 1 号点 | 168≡168 / 168≡168 / 300≡300 / 167≡167 |
⚠⚠ 而档 2 顺手演了第 7 章 P1638 那条: 为①Max 精心造的那一档(一条链、编号不打乱),正好是④One 的盲区 —— 它把④One 从 300 又打回了精确的 0。 ⇒ ★★ 一张表里必须留着最终档,因为没有哪一个单独的档位能同时喂饱所有 bug。
6★ 哪一版就已经能过了
| 版本 | 能过吗 | 数字(顶格最坏形状) |
|---|---|---|
| ✗ 每个点各跑一次 BFS | 70 分 | rand 39.53 秒 / 时限 1 秒 |
| ★ 正解(换根 DP) | ✓ | 0.00 秒、6.1 MiB / 125 MiB |
⇒ 正解一点都不卡常(顶格跑完只要几毫秒),功课全在三件事上:
① 那一句换根公式(而且要说得出它为什么长这样);
② 题面最后一句「编号最小」(官方样例就是一组平局,一测就死);
③ 动笔前那两句算术(int 恰好够 1.72 倍、递归深度 4.9 万层)。
这道题挂在 LCA 那一章的题单里,是为了让你先问一句「要不要倍增」 ——
而答案是「不要」:距离和这个量在父子之间只差 n − 2·size[v] 一句话,两趟遍历就完。
⇒ 和同一张题单里的 P2420 凑成一对:
一道用「异或自己抵消」省掉了 LCA,一道用「换根只差一句话」省掉了 LCA ——
★★ 而两道题被同一个生成器习惯(边一律写成 (父亲, 儿子))藏住了同一个 bug。