本讲定位:图论系列第十二讲,也是本系列与顶层通用算法线的一次正面合流——顶层 07 讲的序列差分,在本讲被原样搬到树上。上一讲说”树上任意两点的路径是唯一确定的”,本讲给出把这条路径真正算出来的工具:最近公共祖先(LCA),以及建立在它之上的树上差分。做了六组实测,三个结论值得先记住:倍增 LCA 在随机树上比”一步步往上跳”的暴力慢 2.2 倍——它的全部价值在链状深树上(实测快 543 倍),交叉点在深度 100~1000 之间;倍增 LCA 那五行代码里每一行写错都是 33%~66% 的错误率(漏
swap33.03%、>=写成>66.04%、漏if(u==v)54.73%、最后返回u而不是fa[u][0]45.27%);点差分漏掉d[fa[lca]]--错 67.94%,而把边差分的式子用在点差分上错 100.00%。本讲还会兑现第 10 讲挂着的两笔欠账(P1967 货车运输、严格次小生成树要的”严格次大边”)。文中所有代码经g++ -O2 -std=c++14 -Wall编译运行,P3379、P3128、P3258、P1967 与洛谷官方样例核对通过。前置知识:第 11 讲(硬前置:
fa/dep/siz、那个迭代版prepare起手式、DFS 序与”子树 = 连续区间”);顶层07-prefix-sum-difference.md的序列差分(第 5 节全程与它对照,没学过的话先去看那一讲的差分部分)。第 4.3 节要用到第 10 讲的最大生成树。适合对象:第 2 节标 J⭐ / S(LCA 是 CSP-J 的上限内容、CSP-S 的必备);第 3、4、5 节标 S;第 4.4 节标 S⭐。CSP-J 冲一等的同学至少要会第 2 节的模板和第 3.1 节的距离公式,这两块 J 组已经出现过。
上一讲的结论是:树上任意两点之间有且仅有一条路径。于是”$u$ 到 $v$ 的路径”是个确定的对象。但确定不等于算得出来。看三个问题:
朴素做法都是”顺着路径走一遍”,单次 $O(\text{路径长度})$,最坏 $O(n)$。$m$ 次就是 $O(nm)$——$5\times10^5$ 规模下彻底没戏。
三个问题的共同卡点是同一件事:
想快速处理”$u$ 到 $v$ 的路径”,先要知道这条路径在哪里”拐弯”。
树上路径的形状永远是:从 $u$ 往上走到某个点,再往下走到 $v$。那个”最高点”就是 $u$ 和 $v$ 的最近公共祖先。
给定一棵有根树,$u$ 和 $v$ 的最近公共祖先 $\mathrm{lca}(u,v)$ 是:既是 $u$ 的祖先、又是 $v$ 的祖先的点中,深度最大的那个。
(约定每个点是自己的祖先,所以 $u$ 是 $v$ 的祖先时 $\mathrm{lca}(u,v) = u$。)
它把一条路径切成两段:$u \rightsquigarrow \mathrm{lca}$ 和 $\mathrm{lca} \rightsquigarrow v$,两段都是竖直的(沿着父亲链)。“竖直的路径”好处理——这是本讲所有技巧的共同出发点。
int lcaBrute(int u, int v) {
while (dep[u] > dep[v]) u = fa[u]; // 先把深的提上来
while (dep[v] > dep[u]) v = fa[v];
while (u != v) { u = fa[u]; v = fa[v]; } // 一起往上,直到相遇
return u;
}
三行,非常好写,而且第 2.7 节会告诉你:在随机数据上它比倍增还快。 它的问题只有一个——树是一条链时单次 $O(n)$。