这里写到的是用tarjan与并查集求LCA。
首先回顾一下tarjan算法:
核心代码:
for (int i = head[u]; i; i = edge[i].next)
{
int v = edge[i].to;
tarjan(v);
//递归返回,代表此时已经遍历完以 u 为节点的子树,u 根据每次递归层数,u 是不同的
}
它是一个可以将一张有向图或无向图遍历DFS序然后变成一棵树的算法,它的优点在于每次递归tarjan(v)返回后,会形成一个以 u 为根节点(可以是整棵树的子根)的树,此时它的所有子节点 v1 v2 v3… 都已经遍历完全。
首先,tarjan 求的 LCA 是离线算法,需要存储询问的边(这里用的是邻接表存无向边还有询问的边,并且都需要双向的)
那么求LCA就是利用了tarjan在这方面子节点与根节点的优越性。
1、vis[u]数组:表示以 u 节点为根的子树正在遍历或已经遍历完全。
LCA 核心代码块:
inline void tarjan(int u){
vis[u]=true; //代表现在开始遍历以 u 节点为根的子树
for (int i = head[u]; i; i = edge[i].next)
{
int v = edge[i].to;
if (vis[v]) continue; // 这里有两个作用。①、如果节点 v 的树正在遍历或已经遍历,则不需
// 要进行tarjan。②、对于无向图(求LCA模板时给的是多叉树,是无向 // 的),防止子节点 v 又回到 父节点 u 。
tarjan(v);
pre[v] = u; //可用并查集mix函数代替,下面会讲
}
❤开始遍历有关 u 的询问!
}
重点讲一下实现的原理,这个原理就在于巧妙地利用了tarjan回溯时的过程。
如果一个节点 v 到达了 ❤ 处,说明此时以 v 为根的子树已经遍历完全。v 是后访问的,而此后我们求(u ,v)的LCA时,是这样判断的:
遍历到 v 时,由于vis[u]=true,说明以 u 为节点的子树遍历完全或正在遍历。
①、如果vis[u]=true,u 的子树正在遍历,那么很显然,LCA(u,v)是 pre[u]。由于回溯返回时,树的深度是从大到小的,所以此时的pre[u]是最大的祖先节点。
②、如果 u 的子树完全遍历:由于求的是 u 与 v 的最近公共祖先,所以他们的公共祖先 R 的子树肯定没有遍历完全,那么此时一定是在遍历以 R 为根的子树(下面图解就看的懂了~)且在遍历点 v 。那么此时 pre[u] 一定指向的是根 R 。(你可以想一想,以 R 为根有两条路,左边是 u ,右边有 v ,先遍历 u 的子树,遍历完后,再遍历 v 点的,由于递归返回是从下至上的,那么pre[u]指向的是最近的根节点 R 了)
图解:
倘若此时遍历完了 ⑤ 左半边的子树,那么此时 pre[9]=pre[7]=pre[5]=5(此时pre[5]还是等于 5 的,因为节点 ⑤ 的子树还未遍历完全,所以并没有进行并查集的向上合并。)那么接下来返回到节点 ⑤ 时,再遍历 节点 ⑧ ,你会发现到节点 ⑨ 与节点 ⑧ 是同根状态。
感觉还是没说清楚。。。只能放一些问答了。
1、为什么pre[v]=u可以代替 mix(u,v)?
要注意的是,mix(u,v)中一定要是,pre[pv]=pu(pv 是 v 祖先节点,pu 是 u 的祖先节点),因为我们要的是以 u 为根的节点 v 连接于 u 点,所以一定要是 v 的祖先节点连接 u 的祖先节点。而即便只是 pre[v]=u ,在接下来的find求出答案时,仍然会在 find 中向上找祖先节点的。
2、为什么此时 求得LCA是 pre[u] 时,一定是最近的祖先节点?
因为dfs序递归返回时,节点是从下到上一步一步合并与父亲节点的(并查集中mix函数),所以它维护的一直是当前子树的根,而子树的根是从下往上遍历的。
3、为什么此时 vis[u]=true ,对于遍历到 节点 v 时,LCA(u,v)一定等于 pre[u] ?
u 子树正在遍历时好理解,此时 u 与 v 在一条线上的。
u 子树遍历完全后,假设 u 的父节点是 R ,而由于 u 子树遍历完全,则会有 pre[u]= R 。
那么由于 u 子树的节点,现在全部的pre[]都是等于pre[u]的,而pre[u]等于 R 啦,那么通过find函数后,u 子树包括 u 的全部节点的最近祖先节点都是 R 了(“最近祖先节点” 是由于结论 2 ) 好了现在遍历到 R 子树中的另一个节点 v 时,因为现在还在遍历以 R 为根的子树,而 R 的左半边,u树已经遍历完全了,求 LCA(u,v) 不就是等于pre[u]==R 了吗?
对于上面模板题,代码如下:
#include<bits/stdc++.h>
#define MAXN 500008
using namespace std;
int N, M, S;
int cnt, tot;
int head[MAXN], pre[MAXN], qhead[MAXN], lca[MAXN << 1];
bool vis[MAXN];
struct Edge
{
int to;
int next;
}edge[MAXN << 1];
struct Query
{
int to;
int next;
}q[MAXN << 1];
inline void add(int u, int v)
{
edge[++cnt].to = v;
edge[cnt].next = head[u];
head[u] = cnt;
return;
}
inline void qadd(int u, int v)
{
q[++tot].to = v;
q[tot].next = qhead[u];
qhead[u] = tot;
return;
}
inline int find(int x) {
if (pre[x] == x)
return x;
return pre[x] = find(pre[x]);
}
//inline void mix(int x, int y) {
// int px = find(x), py = find(y);
// if (px != py) //如果用并查集mix,注意一定要是pre[儿子v]连接到pre[父亲u]
// pre[py] = px;
// return;
//}
inline void init()
{
cnt = tot = 0;
memset(q, 0, sizeof(q));
memset(edge, 0, sizeof(edge));
memset(head, 0, sizeof(head));
memset(vis, 0, sizeof(vis));
memset(qhead, 0, sizeof(qhead));
memset(lca, 0, sizeof(lca));
for (int i = 1; i <= N; i++) {
pre[i] = i;
}
return;
}
inline void tarjan(int u)
{
vis[u] = true;
for (int i = head[u]; i; i = edge[i].next)
{
int v = edge[i].to;
if (vis[v]) continue;
tarjan(v);
pre[v] = u;
}
for (int i = qhead[u]; i; i = q[i].next)
{
int v = q[i].to;
if (vis[v]) {
if (i % 2 == 0) {
lca[i - 1] = lca[i] = find(v);
}
else {
lca[i] = lca[i + 1] = find(v);
}
}
}
return;
}
int main()
{
while (~scanf("%d%d%d", &N, &M, &S))
{
init();
int A, B;
for (int i = 1; i <= N-1; i++) {
scanf("%d%d", &A, &B);
add(A, B), add(B, A);
}
for (int i = 1; i <= M; i++) {
scanf("%d%d", &A, &B);
qadd(A, B), qadd(B, A);
}
tarjan(S);
for (int i = 1; i <= M*2; i += 2) {
printf("%d\n", lca[i]);
}
}
}
这里要注意的是:
1、 q 邻接表存储的是询问,那么q[1] q[2] 代表的是同一个询问。比如我要询问的是 LCA(4,5),那么由于我并不知道哪个点先遍历,而导致可能会遗漏(比如 只加入 4–>5 ,询问 4 时,vis[5]=false,并没有得到答案,而需要再加入 5–>4 ,那么询问 5 时,vis[4]=true,就得到答案了~)
2、 q[1] q[2] 代表的是同一个询问,并且可能只有其中一个才存有答案( 上面有解释,因为顺序问题~) 那么再加上tarjan 是离线的,所以需要知道答案输出顺序,而这正好就是 顺序。
比如: q[1] q[2] 是第一个询问的答案 q[3] q[4] 是第二个询问的答案 …