题单 · 习题解析

洛谷 P3128 [USACO15DEC] Max Flow P

★★★ LCA 最常见的**第二个**用法:K 条路径、每条把沿途所有点 +1,最后问哪个点最大 —— 树上差分把「一条路径 +1」压成**四次单点加**(`d[s]++ / d[t]++ / d[lca]-- / d[fa[lca]]--`),⇒ 它就是[第 52 章](/ch/52-tree-diff/)正文那道题的原题;★★★ 而这一页最值钱的是**两个错法的触发条件正好互补**:「忘了 `d[fa[lca]]--`」要**存在某个询问的 LCA 不是根**,「根的父亲写成它自己」要**存在某个询问的 LCA 就是根**(⚠ 而且最大值**唯一地**落在根上)⇒ **把询问全压到一边,必然有一个错法当场瞎掉**(档 1 / 档 2 两个能证的精确的 0)—— [为一个 bug 造的档正是另一个的盲区](/sol/p1638/)最干净的一次,**两个 0 是同一句话的两半**;★★ 而后一个错法本身是「**上一章的正确写法就是这一章的 bug**」的现场:[P3379](/sol/p3379/) 的正解写的正是 `up[0][root] = root`(那儿对,跳过头就停在根),照抄过来 `d[fa[lca]]--` 就变成「又减一次根」(**这一章题单里的第二次**,第一次是 [P2420](/sol/p2420/));★★★ 「触发 ≡ 抓获」在那一列**四档一个不差(111 / 0 / 255 / 140)**,⚠ 而前提是第一层写到位 —— 只写「最大值落在根上」是 146 / 0 / 300 / 193,**加上「而且唯一」才严丝合缝**;★ 反过来「忘了 `fa[lca]`」那一列**没写完**(触发 292 / 300 / 0 / 297,抓获 289 / 300 / 0 / 296);⚠⚠ 外加一档**在验零**的现场:询问全跨过根 ⇒ 根被盖了 K 次 ⇒ **答案恒等于 K,连「什么都不算」的试金石都满分**;★★ 而顶格那张表是这一章**第四次**「顺手造的顶格随机数据什么都看不出来」—— 同样 `N = 5×10⁴`、`K = 10⁵`,暴力在**链**上走 16.7 亿个点(8.03 秒 / 时限 1 秒)、在**随机树**上只走 199 万个(0.02 秒,**比正解还快 3 倍**)

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

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

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ᵢ,以及它们之间路径上的所有牛棚。

输入格式

输入的第一行包含 NK

接下来的 N−1 行每行包含两个整数 xyx ≠ y),描述连接牛棚 xy 的管道。

接下来的 K 行每行包含两个整数 st,描述牛奶泵送路径的端点牛棚。

输出格式

输出一个整数,表示通过谷仓中任何一个牛棚的最大牛奶量。

数据范围

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−44 底下再挂 32。十条路径压下来,4 号牛棚被盖了 9 次。

1★★ 这是 LCA 最常见的第二个用法:树上差分

★★★ 把「一条路径 +1」压成四次单点加

第一个用法是「问两点的 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.cpp★ 正解:倍增求 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

2第一版:每条路径真的一步一步走

p3128Brute.cpp✗ 第一版(也是对拍的标准答案)
p3128GenBig.cpp★ 顶格生成器:rand / chain / star / binary
★★★ 顶格 N = 5×10⁴、K = 10⁵:随机树上暴力比正解还快 3 倍
形状 最大深度 ✗ 暴力一共走多少个点 ✗ 暴力 ★ 正解
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⚠ 四个错法:三个在那四次单点加上,一个在「怎么还原」上

p3128NoFa.cpp✗ 错法①:忘了 d[fa[lca]]--
p3128Twice.cpp✗ 错法②:照抄「路径和」的写法,在 LCA 上减两次
p3128RootSelf.cpp✗ 错法③:根的父亲设成它自己 —— 而这是 P3379 的正确写法
p3128Order.cpp✗ 错法④:子树求和顺着 BFS 序累加
★★★ ①和③的触发条件正好互补 —— 一个要「LCA 不是根」,一个要「LCA 就是根」
它其实在算什么 什么时候才会露馅
①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」

p3128Max.cpp★ 试金石:什么都不算,一律输出 K
p3128Gen.cpp★ 生成器:四个档位
p3128Count.cpp★ 数「有几个询问的 LCA 是根」+「最大值是不是唯一地落在根上」
// 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;
}
点一下即可编辑
输出
点「运行 ▶」看结果
★★★ 300 轮 × 四档(本机实测)
档位 ①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 都问出来