流程
前置操作分为以下几步:
- 使用 $\texttt{DFS}$ 遍历整棵树
- 当一个节点被访问过时, 将其标记为
1
- 当一个节点及其子树(或其本身就是叶子节点)都被访问过时, 将该点标记为
2
这样, 在任意一次操作过后, 都会出现如下情况:
所有的标记为2
的节点, 共同构成一棵子树
且该子树总是在整棵树的左下处
从动态的角度来看, 就是一棵全为2
的子树, 从左下角的那个叶子节点处缓缓地”长”了出来
这样有什么好处呢
这个性质可以帮助我们很好地离线解决$\texttt{LCA}$问题
Solve
建立一个并查集: father[]
在搜索的返回过程中, 将已经变成2
的那个节点的father
设为它的父亲节点(相当于把该节点”缩”到了其父亲节点上)
在搜索的返回前, 即遍历完该节点的子树后, 判断该节点$u$有没有被”提问”过
如果有, 假设提问为$lca(u, v)$, 则标记答案为getfather(v)
, 其中getfather()
函数为并查集自带的查找祖先节点的函数
- 为什么?
根据上边的结论, 此时的getfather(v)
即为那棵全为2
的子树的根节点$root’$, $\because u$和$v$均在这棵子树上, $\therefore root’$为$u$和$v$的公共祖先
考虑$\texttt{DFS}$序, $\because \texttt{DFS}$访问完一个节点不会马上往上走, 而是会去遍历其它节点, $\therefore root’$以下没有一个$root’’$满足是$u$和$v$的公共祖先 $\therefore lca(u, v) = root’$
代码
1 |
|
时间复杂度
$$ \Theta{(n + m)} $$
搜索过程是$\Theta{(n)}$的, 而其中的求解过程可以单独拆出来看, 它就是一个$\Theta{(m)}$
用一张表来分析分析求$\texttt{LCA}$的各种算法的优劣之处:
算法 | 优劣 | 时间复杂度 | 评测结果 |
---|---|---|---|
朴素 | 比较好想2333 | $\Theta{(nm)}$ | |
倍增优化 | 也比较好想, 但是较容易敲挂 | $\Theta{((n + m)logn)}$ | |
Tarjan | 比较精妙, 也不太好写 | $\Theta{(n + m)}$ |