题单 · 习题解析

洛谷 P1395 会议

★★★ 这道题挂在[第 51 章](/ch/51-lca/)的题单里,是为了让你先问一句「要不要倍增」——而答案是**不要**:「某个点到所有点的距离和」在父子之间只差一句话 `f[v] = f[u] + n − 2·size[v]`(往下挪一格:`size[v]` 个人各近一步,剩下的 `n − size[v]` 个各远一步)⇒ **两趟遍历 O(n) 就完**;★★ 动笔前只要两句算术,而两句都是「**恰好够**」:顶格链站在一端是 1 249 975 000 ⇒ **`int` 余量 1.72 倍**,递归深度 49 999 层 ⇒ 离[那道 17.4 万层的门槛](/sol/p5318/)**余量 3.5 倍**(⚠ 而正解照样写 `long long`、照样不用递归 —— 这两样在这道题上代价是零);★★★ 而这一页最漂亮的一格在暴力那张表:**四种顶格形状访问的边数一个不差**(49.999 亿 —— 能算死:每跑一次 BFS 正好走遍 `2(n−1)` 条有向边,**和树长什么样一点关系都没有**),**秒表却差 4.9 倍**(star 8.11 / binary 9.08 / chain 15.45 / **rand 39.53 秒**)⇒ [两把尺子打架](/sol/p1074/)最干净的一次:**两边做的活一模一样,差的只是那些活离得多远**;★★★ 外加对拍表上「触发 ≡ 抓获」**八格全中**,而两条触发条件都写得出来:①「平局取了编号大的」⟺ **这棵树上存在平局**(⟺ 有一条边把它劈成点数相同的两半 ⇒ `n` 必须是偶数)/③「漏了那个 2」⟺ **正解的答案不是 1 号点**(漏了 2 之后 `f` 沿树只增不减 ⇒ 它**恒输出 1 号点**,⚠ 而随机小树上「1 号点恰好就是答案」的比例高达 44%);⚠⚠ 而④「无向边只存一遍」在顺手档又是**结构性的精确的 0**(生成器一律按 `(fa[i], i)` 写边,**正好就是 BFS 要走的方向**)—— **同一个生成器习惯,在这一章里连着藏了两道题的同一个 bug**(另一道是 [P2420](/sol/p2420/));★ 而专为①造的那一档(一条链、编号不打乱)**又把④打回精确的 0** ⇒ [为一个 bug 造的档正是另一个的盲区](/sol/p1638/)

原题:洛谷 P1395出自 第 51 章 LCA 与倍增:把「往上跳多少步」拆成二进制 的题单题面本地存档:2026-09-11
⚠ 先自己写一遍,再往下看

这一页不是标准答案,是一条阶梯:把大多数人真实会写出的第一版摆出来, 看它错在哪、慢在哪,再一步一步改。所以它对「已经自己动手撞过一次墙」的人最有用。
还没写过的话,先去写 —— 写出来的哪怕是错的,也比直接看这一页收获大。

0题目原文(本地存了一份)

为什么要在这儿抄一份题面

原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。

转录自洛谷 P1395,日期见页头。两边不一致时信原站。

题目描述

有一个村庄居住着 n 个村民,有 n−1 条路径使得这 n 个村民的家连通,每条路径的长度都为 1。 现在村长希望在某个村民家中召开一场会议,村长希望所有村民到会议地点的距离之和最小, 那么村长应该要把会议地点设置在哪个村民的家中,并且这个距离总和最小是多少? 若有多个节点都满足条件,则选择节点编号最小的那个点。

输入格式

第一行,一个数 n,表示有 n 个村民。

接下来 n−1 行,每行两个数字 ab,表示村民 a 的家和村民 b 的家之间存在一条路径。

输出格式

一行输出两个数字 xyx 表示村长将会在哪个村民家中举办会议,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 挪到 vv 那棵子树里的 size[v] 个人各了一步, 剩下的 n − size[v] 个人各了一步 ⇒ 净变化 (n − size[v]) − size[v]

一趟 BFS 求出 size[]f[1],再顺着 BFS 序把 f 推下去,O(n) 就完了。P2420 一样,这一页也是「先想清楚这题到底要不要那个大家伙」。

p1395.cpp★ 正解:换根 DP,两趟遍历
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

2⚠ 动笔前的两句算术(这道题只需要这两句)

★★ 一句问「要不要 long long」,一句问「能不能写递归」
要问的 乘一遍 结论
距离和最大多少 顶格 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

p1395Brute.cpp✗ 第一版:O(n²),也是对拍的标准答案
p1395GenBig.cpp★ 顶格生成器:rand / chain / star / binary
★★★ 同样 49.999 亿次边访问(一个不差),四种形状的秒表差 4.9 倍
顶格 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⚠ 四个错法:一个在题面最后一句上,两个在那句公式上,一个在存图上

p1395Max.cpp✗ 错法①:平局取了编号大的(< 写成 <=)
p1395Sign.cpp✗ 错法②:换根那一句的符号推反了
p1395Half.cpp✗ 错法③:漏了那个 2
p1395One.cpp✗ 错法④:无向边只存了一遍
★★★ 说清楚③Half「算了什么」,它的一切表现都是白送的

size[v] ≤ nf[u] + n − size[v] ≥ f[u]它推出来的 f 沿着树往下只增不减 ⇒ 最小值永远落在 1 号点上 ⇒ 它输出的恒是 1 f[1]

⇒ 于是「它什么时候会被抓」是一句白送的推论:当且仅当正解的答案不是 1 号点。 ★ 而这句话在四个档上一个不差(第 ⑥ 步那张表:168 ≡ 168、168 ≡ 168、300 ≡ 300、167 ≡ 167)。

⚠ 反过来说,在随机小树上「1 号点恰好就是答案」的比例高达 44% —— 那正是这个错法能骗过一半对拍轮次的原因。

⚠⚠ 而④One 被顺手写的生成器藏得严严实实(这一章第二次)

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」

p1395Zero.cpp★ 试金石:什么都不算
p1395Gen.cpp★ 生成器:四个档位
p1395Count.cpp★ 数「有几个点同时取到最小值」+ 两条路各做多少活
// 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;
}
点一下即可编辑
输出
点「运行 ▶」看结果
★★★ 300 轮 × 四档(本机实测)
档位 ①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。