LCA 与 tarjan 实现的原理

P3379 LCA模板

这里写到的是用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] 是第二个询问的答案 …

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
LCA(最近公共祖先)是指在一棵树中,找到两个节点的最近的共同祖先节点。而Tarjan算法是一种用于解强连通分量的算法,通常应用于有向图中。它基于深度优先搜索(DFS)的思想,通过遍历图中的节点来构建强连通分量。Tarjan算法也可以用于解LCA问题,在有向无环图(DAG)中。 具体来说,在使用Tarjan算法解LCA时,我们需要进行两次DFS遍历。首先,我们从根节点开始,遍历每个节点,并记录每个节点的深度(即从根节点到该节点的路径长度)。然后,我们再进行一次DFS遍历,但这次我们在遍历的过程中,同时进行LCA的查找。对于每个查询,我们将两个待查询节点放入一个查询列表中,并在遍历过程中记录每个节点的祖先节点。 在遍历的过程中,我们会遇到以下几种情况: 1. 如果当前节点已被访问过,说明已经找到了该节点的祖先节点,我们可以更新该节点及其所有后代节点的祖先节点。 2. 如果当前节点未被访问过,我们将其标记为已访问,并将其加入到查询列表中。 3. 如果当前节点有子节点,我们继续递归遍历子节点。 最终,对于每个查询,我们可以通过查询列表中的两个节点的最近公共祖先节点来解LCA。 需要注意的是,Tarjan算法的时间复杂度为O(V+E),其中V为节点数,E为边数。因此,对于大规模的树结构,Tarjan算法是一种高效的解LCA问题的方法。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值