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ⱼ ≤ n,0 ≤ 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),
上界 = 所有计划里最长的那条路径。
设「超时的计划」= 长度 > 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 [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;}点「运行 ▶」看结果
2从暴力爬上来:三级台阶,而中间那一级值 50 分
| 复杂度 | 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 条航道连接 i 与 i+1」,
那正好就是一条链。
⇒ ★★★ 「顶格 ≠ 最坏」在这道题上是出题人明写在表格里的:
那一列「约定」不是背景说明,是出题人告诉你他会卡什么形状。
3★ 顶格:正解自己的余量只有 1.9 倍,而题面已经提醒过了
| 形状 | 最大深度 | 最长路径 | 二分轮数 | ★ 正解 | ✗ ④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 的错法
| 官方样例的输出 | ||
|---|---|---|
| ★ 正解 | 11 | |
| ✗ ①NoGain | 11 | ⚠ 放过 |
| ✗ ②Point | 8 | ★ 一测就死 |
| ✗ ③Lo1 | 11 | ⚠ 放过(这组样例的答案不是 0) |
| ✗ ④Rebuild | 11 | ⚠ 放过(它本来就永远对) |
⇒ 又一次「样例是个『一测就死』的过滤器」: 它挡住的是「每组都错」的那一个,放过的三个都是「偶尔才错 / 只是慢」的。 ⇒ ★★ 而真正让你 WA 在第 7 个点上的,恰恰是后一种。
5★ 对拍:五个档位,而其中两个 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;}点「运行 ▶」看结果
| 档位 | ①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」是一句乘法,而这次答案是「不用」
题面顶格:路径最长 (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 只用上了刚算出来的那个量,就该回头问一句:
题目要的条件,是不是还有一半没写。