0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P3128,日期见页头。两边不一致时信原站。
题目描述
Farmer John 在他的谷仓中安装了 N−1 条管道,用于在 N 个牛棚之间运输牛奶(2 ≤ N ≤ 50000),
牛棚方便地编号为 1…N。每条管道连接一对牛棚,所有牛棚通过这些管道相互连接。
FJ 正在 K 对牛棚之间泵送牛奶(1 ≤ K ≤ 100000)。对于第 i 对牛棚,
你被告知两个牛棚 sᵢ 和 tᵢ,这是牛奶以单位速率泵送的路径的端点。
FJ 担心某些牛棚可能会因为过多的牛奶通过它们而不堪重负,因为一个牛棚可能会作为许多泵送路径的中转站。
请帮助他确定通过任何一个牛棚的最大牛奶量。
如果牛奶沿着从 sᵢ 到 tᵢ 的路径泵送,那么它将被计入端点牛棚 sᵢ 和 tᵢ,以及它们之间路径上的所有牛棚。
输入格式
输入的第一行包含 N 和 K。
接下来的 N−1 行每行包含两个整数 x 和 y(x ≠ y),描述连接牛棚 x 和 y 的管道。
接下来的 K 行每行包含两个整数 s 和 t,描述牛奶泵送路径的端点牛棚。
输出格式
输出一个整数,表示通过谷仓中任何一个牛棚的最大牛奶量。
数据范围
2 ≤ N ≤ 5×10⁴,1 ≤ K ≤ 10⁵。
时限 1 秒,内存 128000 KB(125 MiB)。
输入输出样例
输入
5 10 3 4 1 5 4 2 5 4 5 4 5 4 3 5 4 3 4 3 1 3 3 5 5 4 1 5 3 4
输出
9
★ 这棵树是 1−5−4,4 底下再挂 3 和 2。十条路径压下来,4 号牛棚被盖了 9 次。
1★★ 这是 LCA 最常见的第二个用法:树上差分
第一个用法是「问两点的 LCA / 距离」(P3379); 这道题是第二个:K 条路径,每条把沿途所有点 +1,最后问哪个点最大。
一条一条走是 O(K × 深度),顶格 10⁵ × 5×10⁴ = 50 亿。而树上差分只要四次单点加:
d[s]++, d[t]++, d[lca]--, d[fa[lca]]--最后自底向上把子树和累起来,d[u] 就是「u 被盖了多少次」。⇒ O((N + K) log N)。
★ 为什么是「减两次」:d[s] 和 d[t] 各自把「根到自己」那一整路都 +1 了,
于是根到 lca 那一段被加了两遍。而 lca 自己在路径上、要留一份
⇒ 在 lca 减一次(把它从两份压回一份),再在 lca 的父亲减一次(把上面那一整段彻底抹平)。
⇒ ★ 这道题就是第 52 章树上差分正文那道题的原题(那儿只问最大值,这儿也是)—— ⚠ 而第 51 章的题单里就点了这个洞:「这是 LCA 最常见的第二个用法」。
// P3128 [USACO15DEC] Max Flow P —— 正解:倍增求 LCA + **树上差分**,最后一趟子树求和//// ★★★ 这是 LCA 最常见的**第二个**用法(第一个是「问两点的 LCA / 距离」):// K 条路径,每条把沿途所有点 +1,最后问哪个点最大。// 一条一条走是 O(K × 深度),顶格 10⁵ × 5×10⁴ = **50 亿**;// 而树上差分把「一条路径 +1」压成**四次单点加**://// d[s]++, d[t]++, d[lca]--, d[fa[lca]]--//// 最后自底向上把子树和累起来,每个点的值就是它被盖了多少次。⇒ O((N + K) log N)。//// ★ 为什么是「减两次」而不是「减一次」:`d[s]` 和 `d[t]` 各把**根到自己**那一路都 +1 了,// 于是根到 `lca` 那一段被加了两遍 —— `lca` 自己要**留一份**(它在路径上),// 所以在 `lca` 减一次、在 `lca` 的**父亲**再减一次,把上面那一整段彻底抹平。//// ⚠⚠ 而 `fa[root]` 这一处正是[第 52 章](/ch/52-tree-diff/)那条// 「**上一章的正确写法就是这一章的 bug**」的现场:// [P3379](/sol/p3379/) 的正解写的是 `up[0][root] = root`(根的父亲是它自己,跳过头就停在根),// 照抄到这儿,`lca` 恰好是根时 `d[fa[lca]]--` 就变成**又减了一次 `d[root]`**。// ⇒ 这里必须让根的父亲是 **0 号点**(一个不存在的点,专门收这笔账)。
#include <bits/stdc++.h>using namespace std;
const int MAXN = 50005;const int LOG = 17; // 2^16 = 65536 > 5×10⁴
int head_[MAXN], nxt_[MAXN * 2], to_[MAXN * 2], ecnt;int up[MAXN][LOG], dep[MAXN], order_[MAXN], d[MAXN];int n, k;
inline void addEdge(int u, int v) { to_[++ecnt] = v; 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];}
int main() { if (scanf("%d %d", &n, &k) != 2) return 0; for (int i = 0; i < n - 1; i++) { int x, y; if (scanf("%d %d", &x, &y) != 2) return 0; addEdge(x, y); addEdge(y, x); // ⚠ 无向边,两边都要存 }
/* BFS 求 dep 和 up[·][0](⚠ 别用递归,顶格链有 5 万层)*/ { vector<char> vis(n + 1, 0); int cnt = 0; order_[cnt++] = 1; vis[1] = 1; dep[1] = 1; up[1][0] = 0; // ⚠⚠ 根的父亲是 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; 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];
for (int i = 0; i < k; i++) { int s, t; if (scanf("%d %d", &s, &t) != 2) return 0; int l = lca(s, t); d[s]++; d[t]++; d[l]--; if (up[l][0]) d[up[l][0]]--; // ★ l 是根时 up[l][0] == 0,这笔账没人要收 }
/* 自底向上把子树和累起来 —— ⚠ 必须**逆着** BFS 序走 */ int ans = 0; for (int i = n - 1; i >= 0; i--) { int u = order_[i]; ans = max(ans, d[u]); if (up[u][0]) d[up[u][0]] += d[u]; } printf("%d\n", ans); return 0;}点「运行 ▶」看结果
2第一版:每条路径真的一步一步走
| 形状 | 最大深度 | ✗ 暴力一共走多少个点 | ✗ 暴力 | ★ 正解 |
|---|---|---|---|---|
star |
2 | 299 995 | ★ 0.01 秒 | 0.04 |
rand |
26 | 1 989 531 | ★ 0.02 秒 | 0.06 |
binary |
16 | 2 618 135 | ★ 0.03 秒 | 0.05 |
chain |
50 000 | ⚠ 1 666 485 445 | ⚠ 8.03 秒 | 0.11 |
⇒ ★★★ 同样顶格,只换树的形状,暴力走的点数差 838 倍(16.7 亿 vs 199 万)。
而在随机树上它比正解还快 3 倍 —— 因为随机树的深度只有 log 级,
每条路径平均只有 20 个点,而正解要先花 0.04 秒建那张 up[5×10⁴][17] 的表。
⇒ ★★ 这是第 51 章第 3 步那句话在这一章题单里的第四道题上又成立一次 (P3379 快 7.5 倍、P5903 快 1.3 倍、P2420 打平、这道题快 3 倍)。 ⇒ 顺手造一组顶格随机数据然后说「暴力过不了」,这一章里四次都会被打脸。
3⚠ 四个错法:三个在那四次单点加上,一个在「怎么还原」上
| 它其实在算什么 | 什么时候才会露馅 | |
|---|---|---|
| ①NoFa | 根到 fa[lca] 那一段少抹了一层 ⇒ lca 以上的每个祖先都多算 1 |
存在某个询问的 LCA 不是根(是根的话它上面没人,这笔账本来就没有) |
| ③RootSelf | lca 是根时又减了一次 d[root] ⇒ 根被少算了 |
存在某个询问的 LCA 就是根,⚠ 而且最大值唯一地落在根上 |
⇒ ★★★ 于是「为①造的那一档」正好是③的精确的 0,反过来也一样(第 ⑤ 步那张表的档 1 和档 2)。 ⇒ 这是第 7 章 P1638「为一个 bug 精心造的档位正是另一个 bug 的盲区」 在同一页上出现的最干净的一次 —— 两个 0 都能一句话说清,而且是同一句话的两半。
★★ 而③本身是第 52 章那条
「上一章的正确写法可能就是这一章的 bug」的现场:
P3379 的正解写着 up[0][root] = root(跳过头就停在根,那是对的),
照抄到这儿,d[fa[lca]]-- 就变成了「又减一次根」。
⇒ 这一章题单里它是第二次(第一次是 P2420:加法要减两遍,异或一遍都不用减)。
4★ 对拍:四个错法 + 一份「一律输出 K」
// P3128 的两笔账:这份数据长什么样,以及两条路各做多少次基本动作(机器无关)//// 用法:./p3128Count <csv|table> < 一份输入//// ★ 顺手数两个决定成败的东西:// ① **有几个询问的 LCA 就是根** —— 那是错法③的触发条件,而它的补集是错法①的;// ② **最大值是不是落在根上** —— 错法③还要过这一关才会被看见。#include <bits/stdc++.h>using namespace std;
const int MAXN = 50005;const int LOG = 17;static int head_[MAXN], nxt_[MAXN * 2], to_[MAXN * 2], ecnt;static int up[MAXN][LOG], dep[MAXN], order_[MAXN], d[MAXN];static int n, k;
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];}
int main(int argc, char** argv) { string mode = argc > 1 ? argv[1] : "table"; if (scanf("%d %d", &n, &k) != 2) return 0; for (int i = 0; i < n - 1; i++) { int x, y; if (scanf("%d %d", &x, &y) != 2) return 0; to_[++ecnt] = y; nxt_[ecnt] = head_[x]; head_[x] = ecnt; to_[++ecnt] = x; 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; 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 bruteSteps = 0, lcaRoot = 0; for (int i = 0; i < k; i++) { int s, t; if (scanf("%d %d", &s, &t) != 2) return 0; int l = lca(s, t); if (l == 1) lcaRoot++; bruteSteps += (long long)(dep[s] - dep[l]) + (dep[t] - dep[l]) + 1; // 路径上有几个点 d[s]++; d[t]++; d[l]--; if (up[l][0]) d[up[l][0]]--; } /* 自底向上把子树和累起来 —— 累完之后 d[u] 就是「u 被盖了多少次」 */ for (int i = n - 1; i >= 1; i--) { int u = order_[i]; d[up[u][0]] += d[u]; } int ans = 0, ties = 0, argmaxRoot = 0; for (int i = 1; i <= n; i++) ans = max(ans, d[i]); for (int i = 1; i <= n; i++) if (d[i] == ans) ties++; argmaxRoot = (d[1] == ans);
/* 倍增那条路的基本动作:建表 n·(LOG−1) 次写 + 每个询问最多 2·LOG 次读 */ long long fastSteps = (long long)n * (LOG - 1) + (long long)k * 2 * LOG;
vector<pair<string, string> > out; out.push_back(make_pair("n", to_string(n))); out.push_back(make_pair("k", to_string(k))); out.push_back(make_pair("maxdep", to_string(maxDep))); out.push_back(make_pair("ans", to_string(ans))); out.push_back(make_pair("lca_root", to_string(lcaRoot))); out.push_back(make_pair("argmax_root", to_string(argmaxRoot))); out.push_back(make_pair("ties", to_string(ties))); out.push_back(make_pair("brute_steps", to_string(bruteSteps))); out.push_back(make_pair("fast_steps", to_string(fastSteps))); out.push_back(make_pair("ratio", to_string(bruteSteps / 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;}点「运行 ▶」看结果
| 档位 | ①NoFa | ②Twice | ③RootSelf | ④Order | 试金石 |
|---|---|---|---|---|---|
| 0 顺手写法(随机小树、询问随便挑两点) | 289 | 289 | 111 | 299 | 289 |
| 1 询问全取「同一棵子树里的两个点」 | ★ 300 | 272 | ★ 0 | 133 | 300 |
| 2 询问全「跨过根」 | ★ 0 | 255 | 255 | 289 | ⚠ 0 |
| 3 最终档(三种询问混着来) | 296 | 286 | 140 | 271 | 296 |
★★★ 档 1 和档 2 那两个 0 是同一句话的两半:①要「LCA 不是根」,③要「LCA 就是根」 ⇒ 把询问全压到一边,必然有一个错法当场瞎掉。
⚠⚠ 而档 2 还顺手演了另一件事:试金石在那一档是满分(0 处不一致) ——
所有路径都跨过根 ⇒ 根被盖了 K 次 ⇒ 答案恒等于 K,「什么都不算」也全对。
⇒ 「一致有两种:都算对了,和都没算」:那一档在验零,
它只回答得了「①和③谁瞎了」,回答不了「你的算法对不对」。
★★★ 而「触发 ≡ 抓获」在③那一列四档一个不差(111 / 0 / 255 / 140), ⚠ 前提是第一层要写到位 —— 只写「最大值落在根上」是 146 / 0 / 300 / 193(差 35、45、53), 加上「而且唯一」才严丝合缝。 ★ 反过来①那一列没写完:触发 292 / 300 / 0 / 297,抓获 289 / 300 / 0 / 296(差 3 和 1)—— 那 3 轮里它多算的那一层没能把最大值顶上去。 ⇒ ★★ 「能不能写成 ≡,取决于你第一层写得够不够细」,这一页两种结局各演了一次。
5★ 哪一版就已经能过了
| 版本 | 能过吗 | 数字(顶格最坏形状) |
|---|---|---|
| ✗ 每条路径一步一步走 | ✗(⚠ 但随机数据上看不出来) | chain 8.03 秒 / 时限 1 秒 |
| ★ 正解(LCA + 树上差分) | ✓ | chain 0.11 秒、9.0 MiB / 125 MiB |
⇒ 这道题的三件功课:
① 那四次单点加(尤其是「为什么减两次、而且第二次减在父亲身上」);
② 根的父亲必须是 0 号点,不能照抄上一道题的 up[0][root] = root;
③ 还原时必须逆着 BFS 序(顺着走 = 父亲比儿子先结账)。
★ 而「要不要 long long」这次不用想:答案最大就是 K = 10⁵。
这道题是 LCA 的第二个用法,而它的坑一个都不在「怎么求 LCA」上 —— 三个在那四次单点加上,一个在「怎么把差分还原回去」上。 ⇒ 而最值钱的是那对互补的触发条件: 「LCA 不是根」和「LCA 就是根」各自喂饱一个错法、各自饿死另一个 ⇒ ★★ 一张对拍表里必须留着把两边混起来的最终档, 因为没有哪一个极端档能同时把两个 bug 都问出来。