Tarjan 求 LCA
Tarjan 算法可以在 的时间复杂度下,离线地处理 LCA 查询。其中 是节点数量, 是询问次数。
与倍增法比较:
过程
tarjan() 实际上就是一个 DFS。
-
创建一个并查集。注意这个并查集只能用路径压缩来优化,不能用按秩合并。因为我们要严格控制节点的上下级关系。
-
从根节点开始
tarjan()。对于我们搜索到的每个节点 ,执行以下操作:- 递归搜索所有没访问过的、与 相连的节点。
- 回溯的时候,我们将正在遍历的子节点 的父亲设为 。(
fa[v] = u) - 所有子节点搜索完成后,检索涉及 的询问。如果存在询问 ,则检查是否已访问 (
if(vis[v]))。如果已访问 ,记录 。
注意
vis[] 是在 tarjan() 开头设置的,而 fa[] 是退出子节点的 tarjan() 后设置的,有区别!
编码
由于我们要离线处理询问,所以不妨使用链式前向星存储查询(和存边道理类似)。我们还需要为每个节点维护一个 qHead,即询问链表的头。
struct Node {
// ...
int qHead;
}
struct Query {
int to, next, id; // id 是查询编号
} query[MAXM << 1 | 1]; // 两倍空间
int qCnt = 1;
void addQuery(int u, int v) {
query[qCnt].to = v;
query[qCnt].next = node[u].qHead;
node[u].qHead = qCnt++;
}
int ans[MAXM + 1]; // ans[i] 就是编号为 i 的查询的答案
// 然后这样添加查询:
addQuery(u, v);
addQuery(v, u);
下面是主体部分:
void tarjan(int rt) {
node[rt].vis = true;
int v;
for (int e = node[rt].head; e; e = edge[e].next) {
v = edge[e].to;
if (node[v]) continue;
tarjan(v);
fa[v] = rt; // 并查集合并
}
// 可以开始处理查询了
for (int q = node[u].)
}
原理分析
请看上面的树(编号就是 DFS 序),假设我们现在遍历到了 ,当我们完成子树的遍历时,开始处理查询:
-
已经搜索过了,所以它们都在并查集中,且终极祖先都是当前节点 。
-
假如我们现在要处理查询 ,显然
vis[3] == true,所以 。
再比如,我们什么时候会处理查询 呢?
-
当我们访问到 时,
vis[6] == false,所以我们不知道 的 是谁。 -
当我们访问到 时,显然
vis[3] == true。所以 。
总结一下 vis[] 和并查集的作用:
-
vis[v]维护的是:节点 是否已被访问过。如果被访问过,说明节点 和当前节点 的 一定已经被搜索了。你可能会问,有可能 和当前节点的 已经被访问过了,但是vis[v] == false啊。不要紧,之后我们肯定会进入 节点处理查询,这样就转化为第一种情况了。 -
而 维护的是:节点 和当前节点 的那个 是谁。