题单 · 习题解析

洛谷 P2680 [NOIP 2015 提高组] 运输计划

★★★ 第 9 章的二分、第 51 章的 LCA、第 52 章的边差分焊在一起,而**三样各自都不难,难的是 `check(T)` 里的那个「并且」**:「虫洞必须被所有超时计划共同经过」是边差分数出来的(`d[u]++, d[v]++, d[lca] -= 2`),而「**省下来的时间够不够**」(`w(v) ≥ maxBad − T`)**不在差分的输出里** —— ⚠ 漏掉后半句,**官方样例照过不误**(四个错法里样例只挡住一个);★★★ 「二分下界写成 1」的触发 ≡ 抓获**五档一个不差**(答案恰好为 0 的轮数 7/300/3/82/15,逐格相同),而「边差分写成点差分」在「计划全跨过根」那一档是**能证的精确的 0**(少减的那个 1 记在根头上,而**根头上没有边**);⚠⚠ 而它反过来的档触发 243 只抓 40 ⇒ **触发是必要条件不是充分条件**;★★ 顶格 `n = m = 3×10⁵` 上**正解自己只剩 1.9 倍余量**(链上 0.53 秒 / 时限 1 秒,二分 28 轮 vs 菊花 11 轮)—— 题面末尾那句「请注意常数因子」是认真的:把倍增表建进二分里就是 3.13 秒;★★★ 而三级台阶那张表把[「顶格 ≠ 最坏」](/sol/p3916/)**写进了出题人的表格**:`n = m = 3000` 上暴力在链上 17.87 秒、随机树上 0.16 秒(差 112 倍),而题面测试点 5/6/7 的「约定」一栏明写着「第 i 条航道连接 i 与 i+1」;★ 外带一条算术:路径最长 3×10⁸ ⇒ **`int` 就够,余量 7.16 倍**(顶格四种形状 int 版和 long long 版逐字节相同,而把边权上界抬到题面之外当场分道扬镳 —— 那就是这个 0 的自检)

⚠ 先自己写一遍,再往下看

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

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

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

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

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

题目背景

NOIP2015 Day2T3

题目描述

公元 2044 年,人类进入了宇宙纪元。

L 国有 n 个星球,还有 n−1 条双向航道,每条航道建立在两个星球之间, 这 n−1 条航道连通了 L 国的所有星球。

小 P 掌管一家物流公司,该公司有很多个运输计划,每个运输计划形如: 有一艘物流飞船需要从 uᵢ 号星球沿最快的宇航路径飞行到 vᵢ 号星球去。 显然,飞船驶过一条航道是需要时间的,对于航道 j,任意飞船驶过它所花费的时间为 tⱼ, 并且任意两艘飞船之间不会产生任何干扰。

为了鼓励科技创新,L 国国王同意小 P 的物流公司参与 L 国的航道建设, 即允许小 P 把某一条航道改造成虫洞,飞船驶过虫洞不消耗时间。

在虫洞的建设完成前小 P 的物流公司就预接了 m 个运输计划。在虫洞建设完成后, 这 m 个运输计划会同时开始,所有飞船一起出发。当这 m 个运输计划都完成时, 小 P 的物流公司的阶段性工作就完成了。

如果小 P 可以自由选择将哪一条航道改造成虫洞,试求出小 P 的物流公司完成阶段性工作所需要的最短时间是多少?

输入格式

第一行包括两个正整数 n, m,表示 L 国中星球的数量及小 P 公司预接的运输计划的数量, 星球从 1 到 n 编号。

接下来 n−1 行描述航道的建设情况,其中第 i 行包含三个整数 aᵢ, bᵢtᵢ, 表示第 i 条双向航道修建在 aᵢbᵢ 两个星球之间,任意飞船驶过它所花费的时间为 tᵢ

接下来 m 行描述运输计划的情况,其中第 j 行包含两个正整数 uⱼvⱼ, 表示第 j 个运输计划是从 uⱼ 号星球飞往 vⱼ 号星球。

输出格式

一个整数,表示小 P 的物流公司完成阶段性工作所需要的最短时间。

数据范围

所有测试数据的范围和特点如下表所示:

测试点编号 n = m = 约定
1 100 1
2 100 100 i 条航道连接 i 号星球与 i+1 号星球
3 100 100
4 2000 1
5 1000 1000 i 条航道连接 i 号星球与 i+1 号星球
6 2000 2000 i 条航道连接 i 号星球与 i+1 号星球
7 3000 3000 i 条航道连接 i 号星球与 i+1 号星球
8 1000 1000
9 2000 2000
10 3000 3000
11 80000 1
12 100000 1
13 70000 70000 i 条航道连接 i 号星球与 i+1 号星球
14 80000 80000 i 条航道连接 i 号星球与 i+1 号星球
15 90000 90000 i 条航道连接 i 号星球与 i+1 号星球
16 100000 100000 i 条航道连接 i 号星球与 i+1 号星球
17 80000 80000
18 90000 90000
19 100000 100000
20 300000 300000

所有数据满足 1 ≤ aᵢ, bᵢ, uⱼ, vⱼ ≤ n0 ≤ tᵢ ≤ 1000

请注意常数因子带来的程序效率上的影响。

时限 1 秒,内存 262144 KB(256 MiB)。

输入输出样例

输入

6 3 
1 2 3 
1 6 4 
3 1 7 
4 3 6 
3 5 5 
3 6 
2 5 
4 5

输出

11

★ 三个计划本来是 3→6 = 11、2→5 = 15、4→5 = 11,最长 15。 把 1—3 那条时间 7 的航道改成虫洞 ⇒ 4 / 8 / 11,最长 11。 ⚠ 而挖 3—5 那条 5 也一样得 11 —— 挖哪条不唯一,可题目只要那个数

1★★★ 二分那一半好想,check 那一半才是整道题

★★ 先把「二分答案」这一步的理由说清楚

问的是「最长的那个计划要花多久」,而它有单调性: 如果 T 秒能做到(存在一条航道,挖了它之后所有计划都 ≤ T),那 T+1 秒当然也能做到。 ⇒ 二分 T,把「求最小值」变成「判一句 T 秒够不够」。

★ 二分的范围也是现成的:下界 0(⚠ 题面写着 0 ≤ tᵢ,航道时间可以是 0), 上界 = 所有计划里最长的那条路径。

★★★ check(T):两句话,而所有人第一遍都只写了第一句

设「超时的计划」= 长度 > T 的那些,一共 c 条,其中最长的那条是 maxBad

那条虫洞必须被这 c 条路径全都经过。 只能挖一条边,谁没被挖到谁就还是超时。 ⇒ 「一条边被几条路径经过」正是边差分

   d[u]++,  d[v]++,  d[lca] -= 2

再一趟子树求和,d[v] 就是「v 和它父亲之间那条边被盖了几次」。要找 d[v] == c 的边。

⚠⚠ ② 光被全覆盖还不够,省下来的时间得够多。 挖掉边 e 之后,最长的那条超时计划变成 maxBad − w(e),这个数也要 ≤ T

⇒ 合起来是一句:d[v] == c && w(v) >= maxBad - T。 ★★★ 漏掉后半句,就是这道题最经典的一发 WA —— 而且它的官方样例照过不误(见第 ④ 步)。

★ 为什么边差分是 d[lca] -= 2 而不是点差分d[l]--, d[fa[l]]--: 点差分数的是「这个点被盖了几次」,边差分数的是「它头上那条边被盖了几次」—— 而 lca 头上那条边不在路径上,所以两边各抬上来的那 1 都要还回去。

p2680.cpp★ 正解:二分答案 + 边差分 + LCA
// P2680 [NOIP 2015 提高组] 运输计划 —— 正解:**二分答案 + 边差分 + LCA**,三件东西合在一起
//
// ★★★ 这道题是本书第 9 / 51 / 52 三章的合体,而**难点全在「二分之后那半句 check」上**。
//
// 二分那一半好想:答案(最长的那个计划要花多久)具有单调性 ——
// 如果 T 秒能做到,那 T+1 秒当然也能。⇒ 二分 T,问一句「T 秒够不够」。
//
// ★★★ 而 check(T) 那一半是整道题:设「超时的计划」= 长度 > T 的那些,一共 c 条。
// ① 那条虫洞**必须被这 c 条路径全都经过** —— 只挖一条边,谁没被挖到谁就还是超时。
// ⇒ 「一条边被几条路径经过」正是**边差分**:`d[u]++, d[v]++, d[lca] -= 2`,
// 再一趟子树求和,`d[v]` 就是「v 和它父亲之间那条边被盖了几次」。
// ⇒ 要找的是 `d[v] == c` 的边。
// ⚠⚠ ② **光被全覆盖还不够,省下来的时间得够多**:
// 挖掉边 e 之后,最长的那条超时计划变成 `maxBad − w(e)`,这个数**也要 ≤ T**。
// ⇒ 判据是 `d[v] == c && w(v) >= maxBad - T`。**漏掉后半句是这道题最常见的 WA。**
//
// ⇒ 复杂度:一次 LCA 预处理 O(n log n) + 二分 O((n + m) log(Σt))。
// ★ 题面末尾专门写着「**请注意常数因子带来的程序效率上的影响**」——
// 所以倍增表只能建**一次**,绝不能放进二分里(见 p2680Rebuild.cpp)。
//
// ★ 「要不要 long long」是一句乘法:路径最长 (n−1)·max t = 3×10⁵ × 1000 = **3×10⁸**,
// 而 int 的上限是 2 147 483 647 ⇒ **够,余量 7.16 倍**。这一版全程用 int。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 300005;
const int LOG = 19;
int head_[MAXN], nxt_[MAXN * 2], to_[MAXN * 2], wt_[MAXN * 2], ecnt;
int up[MAXN][LOG], dep[MAXN], dist_[MAXN], wpar[MAXN], order_[MAXN], d[MAXN];
int pu[MAXN], pv[MAXN], pl[MAXN], plen[MAXN];
int n, m;
inline void addEdge(int u, int v, int w) { to_[++ecnt] = v; wt_[ecnt] = w; nxt_[ecnt] = head_[u]; head_[u] = ecnt; }
int lca(int u, int v) {
if (dep[u] < dep[v]) swap(u, v);
int diff = dep[u] - dep[v];
for (int j = 0; j < LOG; j++) if ((diff >> j) & 1) u = up[u][j];
if (u == v) return u;
for (int j = LOG - 1; j >= 0; j--) if (up[u][j] != up[v][j]) { u = up[u][j]; v = up[v][j]; }
return up[u][0];
}
/* T 秒够不够? */
bool check(int T) {
int c = 0, maxBad = 0;
for (int i = 0; i < m; i++) if (plen[i] > T) { c++; maxBad = max(maxBad, plen[i]); }
if (c == 0) return true; // ★ 一条都没超时,连虫洞都不用挖
memset(d, 0, sizeof(int) * (n + 1));
for (int i = 0; i < m; i++) if (plen[i] > T) { d[pu[i]]++; d[pv[i]]++; d[pl[i]] -= 2; }
for (int i = n - 1; i >= 1; i--) d[up[order_[i]][0]] += d[order_[i]];
for (int i = 1; i < n; i++) {
int v = order_[i]; // ⚠ order_[0] 是根,跳过(根头上没有边)
if (d[v] == c && wpar[v] >= maxBad - T) return true;
}
return false;
}
int main() {
if (scanf("%d %d", &n, &m) != 2) return 0;
for (int i = 0; i < n - 1; i++) {
int x, y, w;
if (scanf("%d %d %d", &x, &y, &w) != 3) return 0;
addEdge(x, y, w); addEdge(y, x, w);
}
{
vector<char> vis(n + 1, 0);
int cnt = 0;
order_[cnt++] = 1; vis[1] = 1; dep[1] = 1; up[1][0] = 0; dist_[1] = 0;
for (int i = 0; i < cnt; i++) {
int u = order_[i];
for (int e = head_[u]; e; e = nxt_[e]) {
int v = to_[e];
if (vis[v]) continue;
vis[v] = 1; dep[v] = dep[u] + 1; up[v][0] = u;
dist_[v] = dist_[u] + wt_[e]; wpar[v] = wt_[e];
order_[cnt++] = v;
}
}
}
for (int j = 1; j < LOG; j++)
for (int v = 1; v <= n; v++)
up[v][j] = up[up[v][j - 1]][j - 1];
int hi = 0;
for (int i = 0; i < m; i++) {
if (scanf("%d %d", &pu[i], &pv[i]) != 2) return 0;
pl[i] = lca(pu[i], pv[i]);
plen[i] = dist_[pu[i]] + dist_[pv[i]] - 2 * dist_[pl[i]];
hi = max(hi, plen[i]);
}
int lo = 0; // ⚠ 下界是 0,不是 1:t 可以全为 0
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (check(mid)) hi = mid; else lo = mid + 1;
}
printf("%d\n", lo);
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

2从暴力爬上来:三级台阶,而中间那一级值 50 分

p2680Brute.cpp✗ 第一版(也是对拍的标准答案):枚举每条边,再沿树爬一遍
p2680Half.cpp★ 第二版:LCA 先把长度算好,边只是一个 O(1) 的判断 —— O(nm)
★★ 三级台阶各值多少分(本机实测)
复杂度 n = m = 3000 n = m = 3000(随机) 顶格 3×10⁵
✗ 第一版 O(n × m × 深度) 17.87 秒 0.16 秒 9.0×10¹⁵ 步
★ 第二版 O(nm) 0.03 秒 0.04 秒 9×10¹⁰ 步
正解 O((n+m) log Σt) 0.00 秒 0.00 秒 0.53 秒

(本机 · A 机 WSL2 · nproc 12 · 2026-09-12 · 独占 · 每格 3 次取中位数。)

⇒ ★★ 第二版稳拿 50 分 —— 题面的测试点 1~10 全都是 n, m ≤ 3000。 ⇒ ⚠⚠ 而第一版连这 50 分都拿不全n = m = 3000 上,链要 17.87 秒、随机只要 0.16 秒, 差 112 倍 —— 而题面的测试点 5 / 6 / 7 标着「第 i 条航道连接 ii+1」, 那正好就是一条链。 ⇒ ★★★ 「顶格 ≠ 最坏」在这道题上是出题人明写在表格里的: 那一列「约定」不是背景说明,是出题人告诉你他会卡什么形状

3★ 顶格:正解自己的余量只有 1.9 倍,而题面已经提醒过了

p2680GenBig.cpp★ 顶格生成器:rand / chain / star / binary,⚠ 还能把边权上界调到题面之外
p2680Read.cpp★ 对照:只把 9.1 MB 输入读进来,什么都不算
p2680Rebuild.cpp✗ 错法④:把倍增表建进了二分循环里 —— 答案永远对
★★★ 顶格 n = m = 3×10⁵(时限 1 秒)
形状 最大深度 最长路径 二分轮数 ★ 正解 ✗ ④Rebuild
star 2 1 999 11 0.19 秒 0.43
binary 19 22 648 14 0.24 秒 0.60
rand 31 29 114 15 0.25 秒 0.64
chain 300 000 150 123 683 28 0.53 秒 3.13 秒

(只读入那 9.1 MB 是 0.05 秒 —— 占正解最坏那一格的 9.4%,减不减都不改变结论。)

⇒ ★★★ 正解自己在链上只剩 1.9 倍余量 —— 而题面末尾那句 「请注意常数因子带来的程序效率上的影响」正是为这件事写的。 ★ 二分轮数由最长路径说了算(log₂ 那个数):菊花上 11 轮,链上 28 轮,差 2.5 倍; 而每一轮都要扫一遍全部 n + m

⇒ ⚠ ④Rebuild 把 n·(LOG−1) ≈ 5.4×10⁶ 次建表乘上这 28 轮 ⇒ 1.5×10⁸ 次写 ⇒ 链上 3.13 秒,超时 3.1 倍; ⚠⚠ 而它在菊花上只要 0.43 秒 —— 它是那种「随机数据上完全看不出来」的错法, 而它的答案一个字都不错(下面那张对拍表里它五档全是 0)。

4⚠ 三个真会 WA 的错法

p2680NoGain.cpp✗ 错法①:只判「被全覆盖」,忘了「省得够不够」
p2680Point.cpp✗ 错法②:边差分写成了点差分(d[lca] -= 1)
p2680Lo1.cpp✗ 错法③:二分下界从 1 开始
★★★ 官方那组样例,四个错法里只挡住了一个
官方样例的输出
★ 正解 11
✗ ①NoGain 11 放过
✗ ②Point 8 ★ 一测就死
✗ ③Lo1 11 放过(这组样例的答案不是 0)
✗ ④Rebuild 11 放过(它本来就永远对)

⇒ 又一次「样例是个『一测就死』的过滤器」: 它挡住的是「每组都错」的那一个,放过的三个都是「偶尔才错 / 只是慢」的。 ⇒ ★★ 而真正让你 WA 在第 7 个点上的,恰恰是后一种。

5★ 对拍:五个档位,而其中两个 0 是能证的

p2680Zero.cpp★ 试金石:什么都不算,一律输出 0
p2680Gen.cpp★ 生成器:五个档位
p2680Count.cpp★ 数「超时计划里有几个的 LCA 不是根」+「答案是不是 0」
// P2680 的账:这份数据长什么样、答案是多少、二分要问几轮(都是机器无关的数)
//
// 用法:./p2680Count <csv|table> < 一份输入
//
// ★ 顺手数三个决定成败的东西:
// ① **超时计划里有几个的 LCA 不是根** —— 那正是错法②(边差分写成点差分)的触发条件;
// ② **答案是不是 0** —— 那是错法③(二分下界写成 1)的触发条件;
// ③ **最长路径有多长** —— 「要不要 long long」那句乘法的左边。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 300005;
const int LOG = 19;
static int head_[MAXN], nxt_[MAXN * 2], to_[MAXN * 2], wt_[MAXN * 2], ecnt;
static int up[MAXN][LOG], dep[MAXN], wpar[MAXN], order_[MAXN], d[MAXN];
static long long dist_[MAXN];
static int pu[MAXN], pv[MAXN], pl[MAXN];
static long long plen[MAXN];
static int n, m;
static int lca(int u, int v) {
if (dep[u] < dep[v]) swap(u, v);
int diff = dep[u] - dep[v];
for (int j = 0; j < LOG; j++) if ((diff >> j) & 1) u = up[u][j];
if (u == v) return u;
for (int j = LOG - 1; j >= 0; j--) if (up[u][j] != up[v][j]) { u = up[u][j]; v = up[v][j]; }
return up[u][0];
}
static long long badCnt, badNotRootLca;
static bool check(long long T) {
long long c = 0, maxBad = 0;
badNotRootLca = 0;
for (int i = 0; i < m; i++) if (plen[i] > T) {
c++; maxBad = max(maxBad, plen[i]);
if (pl[i] != 1) badNotRootLca++;
}
badCnt = c;
if (c == 0) return true;
memset(d, 0, sizeof(int) * (n + 1));
for (int i = 0; i < m; i++) if (plen[i] > T) { d[pu[i]]++; d[pv[i]]++; d[pl[i]] -= 2; }
for (int i = n - 1; i >= 1; i--) d[up[order_[i]][0]] += d[order_[i]];
for (int i = 1; i < n; i++) {
int v = order_[i];
if (d[v] == c && wpar[v] >= maxBad - T) return true;
}
return false;
}
int main(int argc, char** argv) {
string mode = argc > 1 ? argv[1] : "table";
if (scanf("%d %d", &n, &m) != 2) return 0;
for (int i = 0; i < n - 1; i++) {
int x, y, w;
if (scanf("%d %d %d", &x, &y, &w) != 3) return 0;
to_[++ecnt] = y; wt_[ecnt] = w; nxt_[ecnt] = head_[x]; head_[x] = ecnt;
to_[++ecnt] = x; wt_[ecnt] = w; nxt_[ecnt] = head_[y]; head_[y] = ecnt;
}
{
vector<char> vis(n + 1, 0);
int c = 0;
order_[c++] = 1; vis[1] = 1; dep[1] = 1; up[1][0] = 0;
for (int i = 0; i < c; i++) {
int u = order_[i];
for (int e = head_[u]; e; e = nxt_[e]) {
int v = to_[e];
if (vis[v]) continue;
vis[v] = 1; dep[v] = dep[u] + 1; up[v][0] = u;
dist_[v] = dist_[u] + wt_[e]; wpar[v] = wt_[e]; order_[c++] = v;
}
}
}
for (int j = 1; j < LOG; j++) for (int v = 1; v <= n; v++) up[v][j] = up[up[v][j - 1]][j - 1];
int maxDep = 0;
for (int i = 1; i <= n; i++) maxDep = max(maxDep, dep[i]);
long long hi = 0, lcaRoot = 0;
for (int i = 0; i < m; i++) {
if (scanf("%d %d", &pu[i], &pv[i]) != 2) return 0;
pl[i] = lca(pu[i], pv[i]);
if (pl[i] == 1) lcaRoot++;
plen[i] = dist_[pu[i]] + dist_[pv[i]] - 2 * dist_[pl[i]];
hi = max(hi, plen[i]);
}
long long maxLen = hi, lo = 0, rounds = 0;
while (lo < hi) { long long mid = lo + (hi - lo) / 2; rounds++; if (check(mid)) hi = mid; else lo = mid + 1; }
long long ans = lo;
check(ans); // 把「答案那一轮」的两个计数重新算一遍
long long badAtAns = badCnt, badNotRoot = badNotRootLca;
/* 暴力那条路:n−1 条边 × m 个计划 × 路径长度 */
long long bruteSteps = 0;
for (int i = 0; i < m; i++) {
long long plenNodes = (dep[pu[i]] - dep[pl[i]]) + (dep[pv[i]] - dep[pl[i]]) + 1;
bruteSteps += plenNodes;
}
bruteSteps *= (long long)(n - 1);
long long fastSteps = (long long)n * (LOG - 1) + rounds * (long long)(n + m);
vector<pair<string, string> > out;
out.push_back(make_pair("n", to_string(n)));
out.push_back(make_pair("m", to_string(m)));
out.push_back(make_pair("maxdep", to_string(maxDep)));
out.push_back(make_pair("maxlen", to_string(maxLen)));
out.push_back(make_pair("ans", to_string(ans)));
out.push_back(make_pair("ans_is_zero", to_string(ans == 0 ? 1 : 0)));
out.push_back(make_pair("lca_root", to_string(lcaRoot)));
out.push_back(make_pair("bad_at_ans", to_string(badAtAns)));
out.push_back(make_pair("bad_notroot_lca", to_string(badNotRoot)));
out.push_back(make_pair("rounds", to_string(rounds)));
out.push_back(make_pair("brute_steps", to_string(bruteSteps)));
out.push_back(make_pair("fast_steps", to_string(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(" %-18s %s\n", out[i].first.c_str(), out[i].second.c_str());
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果
★★★ 300 轮 × 五档(本机实测)
档位 ①NoGain ②Point ③Lo1 ④Rebuild 试金石
0 顺手写法 224 26 7 0 293
1 所有 t 都是 0 0 0 300 0 0
2 计划全跨过根 242 0 3 0 297
3 计划全在同一棵子树里 108 40 82 0 218
4 最终档 215 13 15 0 285

★★★ ③Lo1 的「触发 ≡ 抓获」五档一个不差: 「正确答案恰好是 0」的轮数是 7 / 300 / 3 / 82 / 15,和它被抓的轮数逐格相同 —— 这一个是能证的(答案为 0 ⟺ 它打出 1 ⟺ 不同)。

★★★ ②Point 在档 2 是精确的 0,而理由一句话说得完: 所有计划都跨过根 ⇒ 超时计划的 LCA 全是根 ⇒ 少减的那个 1 记在头上, 而根头上没有边 ⇒ 这笔账没有任何一条边看得见。 ⚠ 反过来把计划全压进同一棵子树(档 3),它的触发数从 0 涨到 243, 可抓获只有 40 —— ⇒ ★★ 触发是必要条件,不是充分条件: 多算的那个 1 得真的让它挑中另一条边才算数。

⚠⚠ 而档 1 是这一页在验零的那一格t 全为 0 ⇒ 正确答案恒等于 0 ⇒ 「什么都不算」的试金石满分,①②也跟着满分; ★ 而同一档把③打到了 300 / 300 —— 一个档位,一半在验零、一半在拷问。

6★ 「要不要 long long」是一句乘法,而这次答案是「不用」

p2680LL.cpp★ 对照:把 dist / 路径长度 / 二分那几个量全换成 long long
★★ 3×10⁸ vs 2 147 483 647 —— 够,余量 7.16 倍

题面顶格:路径最长 (n−1) × max t = 3×10⁵ × 1000 = 3×10⁸,而 int 的上限是 2 147 483 647。 ⇒ 正解全程用 int 就够(顶格四种形状 int 版和 long long 版逐字节相同)。

⚠⚠ 而这个「精确的 0」配了自检:把生成器的边权上界从 1000 抬到 100000违反题面), 同一条链上 —— int 版打出 2 147 437 473(死死顶在 2³¹ 下面), long long 版打出 14 961 026 878。⇒ 那段代码是活的,只是题面没让它有机会出事。

★ 而这一条和第 32 章那三道题是同一个动作: 「INF 该写多大 / 要不要 long long」是一道拿题面乘一遍的算术题, 而同一个答案在隔壁那道题上可能就翻面。

7★ 哪一版就已经能过了

★★ 结论
版本 能过吗 数字(顶格最坏形状 chain
✗ 枚举边 + 沿树爬 ✗(50 分都拿不全 n = m = 3000 的链上就要 17.87 秒
★ 枚举边 + LCA 先算长度 ✗(★ 稳拿 50 分 顶格 9×10¹⁰ 步
正解(二分 + 边差分 + LCA) 0.53 秒 / 时限 1 秒、44.3 MiB / 256 MiB

⇒ 这道题的三件功课: ① check(T) 的后半句w(v) >= maxBad − T)—— 漏了它官方样例照样过; ② 边差分是 d[lca] -= 2,不是点差分那两句; ③ 常数因子:倍增表只能建一次;链上二分要 28 轮,余量只剩 1.9 倍。

★ 一句话带走

这道题把第 9 章的二分、第 51 章的 LCA、第 52 章的边差分焊在了一起, 而三样东西各自都不难 —— 难的是那句 check(T) 里的「并且」。 ⇒ ★★ 判据写成「被全覆盖」很自然,因为那正是差分数出来的东西; 而「省下来的时间够不够」不在差分的输出里,要你自己想起来。 ⇒ 看到一个 check 只用上了刚算出来的那个量,就该回头问一句: 题目要的条件,是不是还有一半没写。