Schieber Vishkin Algorithm O(n)-O(1) lca

· · 算法·理论

Schieber Vishkin algorithm 基于状压、位运算 O(n)-O(1) 在线求解 LCA。

讲得不是很清楚的汉化版

核心思想是满二叉树上的快速 LCA。

省流:神秘二进制树剖,快速求 x 子树内包含 y 的儿子。

记号:lb(i) 表示 i 的二进制最低位,hb(i) 表示 i 的二进制最高位,二进制位从 0 开始编号。

以下“x 的祖先”包含 x

满二叉树快速 LCA

观察满二叉树的中序遍历:

观察 xx 的父亲 F_x 二进制的关系。比如 11\to 10\to 12\to 8

\begin{aligned} 11&=101\red1\\ 10&=10\red10\\ 12&=1\red100\\ 8&=\red1000\\ \end{aligned}

注意到 lb(F_x)=lb(x)+1lb(x)x 到叶子的距离。记 x 的高度为其到叶子的距离(lb(x))。

- 第 $lb(x)$ 位变为 $0$。 - 第 $lb(x)+1$ 位必定是 $1$。 人话:从叶子开始,二进制最右端有一个 $1$,每跳一次父亲,$1$ 往左挪一位,原来的位置变为 $0$。 $\gdef\lca{\operatorname{lca}}

求解 \lca(x,y)

\begin{aligned} x&=A\red0?\\ y&=A\red1?\\ \end{aligned}

其中 Ax,y 二进制的 \mathrm{lcp},标红的位是 hb(x\oplus y)

x? 部分全是 0\lca(x,y)=x,特判这种情况。

于是 $\lca(x,y)$ 可以 $O(1)$ 求出。 再观察中序遍历,**结论:$\lca(l,l+1,\dots,r)$ 是 $[l,r]$ 内 $lb$ 最大的数**。这个数显然可以唯一确定。 --- ### 神秘二进制树剖 记 $d_i$ 为 $i$ 的 dfs 入栈序(先序遍历),$f_i$ 为 $i$ 子树内 $lb(d_j)$ 的最大的 $d_j$。 $i$ 子树内的 $d$ 值显然是连续的,故 $f_i$ 唯一。 一棵可能的树: ![](https://s3.bmp.ovh/imgs/2025/07/25/3e6bc371f0d45c73.png) $d$ 树(之后的图都是用 $d$ 重标号过的): ![](https://s3.bmp.ovh/imgs/2025/07/25/642fa5c5f4ec550d.png) $x$ 的实儿子是 $f$ 与 $x$ 相等的儿子,以此树剖。 ![](https://s3.bmp.ovh/imgs/2025/07/25/e8b27e5e45839f11.png) **结论:若 $x$ 在原树上是 $y$ 的祖先,则 $f_x$ 在满二叉树上是 $f_y$ 的祖先。** 显然 $lb(f_x)\ge lb(f_y)$,若 $f_x=f_y$ 显然满足,考虑证明 $f_x\ne f_y$($lb(f_x)>lb(f_y)$): $$ \begin{aligned} f_x&=A\red100\dots000\dots0\\ f_y&=B???\dots\red100\dots0\\ \end{aligned} $$ 标红的位置是 $f_x,f_y$ 的 $lb$。 $d$ 值在 $f_x,f_y$ 之间的点都在 $x$ 的子树内,且 $f_x$ 是其中 $lb$ 最大的。 - $A<B$,$B\red000\dots\red000\dots000$ 比 $f_x$ 的 $lb$ 值更大,矛盾。 - $A>B$,$A\red000\dots\red000\dots000$ 比 $f_x$ 的 $lb$ 值更大,矛盾。 故 $A=B$。 所以 $f_y$ 可以根据满二叉树的跳父亲过程把 $1$ 挪到 $lb(f_x)$ 上,得证。 在满二叉树上 $x$ 的祖先只有 $O(\log n)$ 个,所以每个点到根的路径上至多 $O(\log n)$ 条虚边。 ~~也就是说也可以当正常树剖用,只是每条实链不一定到叶子。~~ --- ### 快速 LCA 原树求 $\lca(x,y)$,不妨设为 $z$,考虑找到满二叉树上 $f_x,f_y$ 的公共祖先 $p$。 但是 $f_z$ 不一定等于 $p$,就比如上面图例中 $\lca(3,7)=1$,但是 $p=4$,$f_1=8$。 根据结论,可以知道 $f_z$ **在满二叉树上**是 $p$ 的祖先。 考虑记录 $S_x$ 表示 $x$ **在原树上**的祖先的 $f$ 值**在满二叉树上**的高度集合。状压存储。 即 $S_x=\{lb(f_i)|i\in anc(x)\}$,$anc(x)$ 为 $x$ **在原树上**的祖先集合。 $lb(f_z)\in S_x$,$lb(f_z)\in S_y$ 且 $lb(f_z)\ge lb(p)$(在满二叉树上比 $p$ 高)。 显然可以通过一系列位运算求出 $f_z$。 那么问题来了,如何由 $f_z$ 倒推回 $z$? 一条实链的 $f$ 值相同,现在知道 $z$ 在哪条实链上。 只需要知道 $x,y$ 到这条实链上的最近点 $x',y'$,$x',y'$ 中深度更小的即为 $z$。 考虑怎么求 $x'$,$y'$ 同理。 $f_x=f_z$ 显然 $x'=x$,只考虑 $f_x\ne f_z$ 的情况。 考虑求出 $x'$ 往 $x$ 方向的儿子(子树内包含 $x$ 的儿子)$m$。$m$ 是其所在实链的链顶。 ![](https://s3.bmp.ovh/imgs/2025/07/27/cd2376ce03b887ce.png) $lb(f_z)$ 和 $lb(f_m)$ 都在 $S_x$ 内,且在 $S_x$ 内 $f_z$ 前驱为 $f_m$,故 $f_m$ 也可以位运算求出。 又 $m$ 是 $f$ 值为 $f_m$ 的链顶,故可以推出 $m$,进而知道 $x'$。 终于,$O(1)$ 求出了 $\lca(x,y)$! 而且求 $m$ 的做法可以 $O(1)$ 求“$x$ 往 $y$ 方向的儿子”。 --- ### 实现 [评测记录](https://www.luogu.com.cn/record/227266542) ```cpp basic_string <int> tr[MAXN]; inline void add_edge(int x,int y){ tr[x].push_back(y); } int lb[MAXS],hb[MAXS]; int dfc=0; int fat[MAXN],top[MAXN],d[MAXN],f[MAXN],s[MAXN]; inline void dfs1(int x){ dfc++; d[x]=dfc; f[x]=dfc; for(int to:tr[x]) { if(to==fat[x]){ continue; } fat[to]=x; dfs1(to); if(lb[f[to]]>lb[f[x]]){ f[x]=f[to]; } } top[f[x]]=x; } inline void dfs2(int x){ s[x]|=1<<lb[f[x]]; for(int to:tr[x]) { if(to^fat[x]){ s[to]=s[x]; dfs2(to); } } } inline int lca(int x,int y){ if(f[x]==f[y]){ return d[x]<d[y]?x:y; } int p=hb[f[x]^f[y]]; int h=lb[(s[x]&s[y])>>p<<p]; int F=(f[x]>>h|1)<<h; if(f[x]^F){ int m=hb[s[x]&((1<<h)-1)]; m=(f[x]>>m|1)<<m; x=fat[top[m]]; } if(f[y]^F){ int m=hb[s[y]&((1<<h)-1)]; m=(f[y]>>m|1)<<m; y=fat[top[m]]; } return d[x]<d[y]?x:y; } ``` [树剖板子评测记录](https://www.luogu.com.cn/record/227279240)