完整部分点这里
首先要说明,Tarjan算法是离线算法,需要在算法流程中读入全部询问,一次dfs出结果,然后再一次性输出来,复杂度为 O(α(n)+Q) O ( α ( n ) + Q ) 。
Tarjan算法的核心思想是先进行一遍深度优先搜索,在讨论 LCA与RMQ的关系 的时候,我们已经论述过 u u 向 v v 遍历过程中深度最小的点就是 LCA(T,u,v) L C A ( T , u , v ) 。举个例子,假设我们求的是 LCA(T,6,8)=1 L C A ( T , 6 , 8 ) = 1 ,那么我们把从 6 6 遍历到 8 8 时涉及到的顶点单独取出来,就可以得到下面一幅图:
从图中,我们可以清晰得看出, 6 6 遍历到 8 8 时深度最小的结点是 1 1 也就是 LCA(T,u,v) L C A ( T , u , v ) 。
下面我们面临的问题就是如何快速求出两个给定结点去所回溯到的最远的点。
我们给每个结点 x x 记录一个 f[x] f [ x ] 值, f[x] f [ x ] 的意义是搜索时曾经从 x x 点回溯到 f[x] f [ x ] 点,初始化时 f[x]←x f [ x ] ← x 。
接下来开始深度优先遍历,当遍历完一个结点的所有子树时,更新 f[x]←x f [ x ] ← x 。同时,就可以处理关于 x x 的一部分询问(有些结点没有被访问过所以不会被处理),很明显的,对于询问 LCA(T,x,y) L C A ( T , x , y ) ,如果 y y 被访问过了,我们就可以认为 LCA(T,u,v) L C A ( T , u , v ) 是 v v 在搜索时曾经回溯到的最远点,至于 f[x] f [ x ] 怎么维护,就要用到并查集了。
这样,一次深度优先遍历之后,我们便得到了所有问题的答案。
下面,我们来模拟一下这个过程:
有根树 T T 最上面已经给出了,而我们面临这样几个询问: LCA(T,5,6),LCA(T,5,3),LCA(T,6,8) L C A ( T , 5 , 6 ) , L C A ( T , 5 , 3 ) , L C A ( T , 6 , 8 ) 。
我们首先搜索到了 1 1 号结点,向下搜索到 2 2 号结点,再从 2 2 号结点搜索到了 5 5 号节点。
由于 5 5 号节点时叶子结点,所以开始处理 5 5 号结点的有关询问,发现 6 6 号节点和 3 3 号节点均未访问,不处理.
回溯到 2 2 号节点, f[5]←2 f [ 5 ] ← 2 。
搜索到 6 6 号节点。
处理 6 6 号结点有关询问,发现 5 5 号节点已经被访问过,于是第一个询问得到处理: LCA(T,5,6)=seek(5)=f[5]=2 L C A ( T , 5 , 6 ) = s e e k ( 5 ) = f [ 5 ] = 2 。
回溯到 2 2 号结点, f[6]←2 f [ 6 ] ← 2 。回溯到 1 1 号节点, f[2]←1 f [ 2 ] ← 1 。
搜索 3 3 号结点。
此时的 f f 数组:
序号 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|
f | 1 | 1 | 3 | 4 | 2 | 2 | 7 | 8 |
- 处理 3 3 号结点有关询问,发现 5 5 号结点已经被访问过了,处理询问, LCA(T,5,3)=seek(5)=f[f[6]]=1 L C A ( T , 5 , 3 ) = s e e k ( 5 ) = f [ f [ 6 ] ] = 1 ,同时因为路径压缩的原因 f[5]←f[2]=1 f [ 5 ] ← f [ 2 ] = 1 。
回溯到 1 1 号节点, f[3]←1 f [ 3 ] ← 1 。
搜索 4 4 号结点,搜索 7 7 号结点,搜索 8 8 号结点
此时的 f f 数组:
序号 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|
f | 1 | 1 | 1 | 4 | 1 | 2 | 7 | 8 |
- 处理与 8 8 号结点有关询问,发现 6 6 号结点已经被访问过了,处理询问, LCA(T,6,8)=seek(6)=f[f[6]]=1 L C A ( T , 6 , 8 ) = s e e k ( 6 ) = f [ f [ 6 ] ] = 1 。因为路径压缩 f[6]←f[2]=1 f [ 6 ] ← f [ 2 ] = 1 。
回溯到 7 7 号结点, f[8]←7 f [ 8 ] ← 7 ,回溯到 4 4 号结点, f[7]=4 f [ 7 ] = 4 ,回溯到 1 1 号结点 f[4]←1 f [ 4 ] ← 1 。
此时的f数组:
序号 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|
f | 1 | 1 | 1 | 1 | 1 | 1 | 4 | 7 |
算法结束。
其实最后的 f f 数组不全部是 root=1 r o o t = 1 的原因就是 7 7 号结点和 8 8 号结点没有进行路径压缩,否则的话所有结点最后的 f f 值都是根节点的编号。
namespace LCA {
edgetype qedge[MAXQ << 1];
int qhead[MAXN], qcnt;
inline void AddQuery(int from, int to, int index) {
qedge[++qcnt] = (edgetype){to, qhead[from], index};
qhead[from] = qcnt;
}
int u[MAXN], v[MAXN], ans[MAXN];
int f[MAXN], dist[MAXN];
bool visit[MAXN];
inline int seek(int u) {
return f[u] == u ? u : f[u] = seek(f[u]);
}
inline void dfs(int u, int p) {
for (int i = head[u]; i; i = edge[i].next) {
int v = edge[i].to;
if (v == p) continue;
dist[v] = dist[u] + edge[i].dist;
dfs(v, u);
f[v] = u;
}
visit[u] = true;
for (int i = qhead[u]; i; i = qedge[i].next) {
int v = qedge[i].to;
if (visit[v])
ans[qedge[i].dist] = seek(v);
}
}
inline void init() {
memset(qhead, 0, sizeof qhead); qcnt = 0;
}
inline void solve(int root, int n) {
dist[root] = 0;
memset(visit, false, sizeof visit);
for (int i = 1; i <= n; i++) f[i] = i;
dfs(root, 0);
}
}
由于Tarjan算 法中存询问和存边所用到的struct的内容和加边函数是一模一样的,为了省事儿,就把两个拼在一起了,否则程序会比较复杂,写起来也麻烦。其实仅仅把这个当成一个像 STL 一样的黑盒代码,只要明白有哪些内置函数和作用就行了,写完赶紧折叠起来,不然强迫症就真的要受不了了(眼不见为净QAQ)。