LCA问题的Tarjan离线算法 + POJ 1470

树的最近公共祖先(Lowest Common Ancestor)问题是树结构上最经典的问题之一。
给一棵树T,每个询问形如:“点u和点v 的公共祖先是哪个点?”,此问题的答案被记为LCA(u, v)。LCA问题的算法分为在线和离线(Tarjan算法)两种,前者要求在回答后一个问题之前必须给出前一个问题的输出,而离线问题允许在读入所有询问之后一次性给出所有问题的答案。

LCA问题的应用很多,例如它可以用来回答这样的询问:“点u和点v的距离是多少?”——由于在树中两点的简单路是唯一的,所以这个距离等于u到LCA(u, v)再到v的距离,关键仍然是LCA。



Tarjan离线算法是基于树的dfs和并查集的,我们令x=LCA(u, v),用这么一句话解释这个算法:如果我们在访问完节点u及其后代后,将u的祖先指向为x,那么在访问x的其它后代节点v时,LCA(u,v)就为u的(当前的)祖先点x

int find(int x) {return fa[x] == x ? x : fa[x] = find(fa[x]);}//x的祖先节点

void tarjan(int u)//在main中调用tarjan(root)
{
	vis[u] = true;
	int i, v;
	for (i = 1; i <= query[u][0]; ++i)//query[u][0]表示形如LCA(u,?)的查询个数
	{
		v = query[u][i];
		if (vis[v]) /*printf LCA(u,v)=find(v) or others*/
	}
	for (i = 1; i <= son[u][0]; ++i)//son[u][0]表示u的儿子个数
	{
		v = son[u][i];
		if (!vis[v]) tarjan(v), fa[v] = u;
	}
}


例题:

POJ 1470 Closest Common Ancestors  

此题样例的函数调用过程如下:

tarjan(5)
	
	tarjan(1)
		++sum[find(5)->5];
	//1 done
	fa[1]=5;
	
	tarjan(4)
		++sum[find(1)->find(5)->5];
	//4 done
	fa[4]=5;
	
	tarjan(2)
		++sum[find(4)->find(5)->5];
		tarjan(3)
			++sum[find(2)->2];//注意此时fa[2]尚未指向5
			++sum[find(1)->find(5)->5];
			++sum[find(4)->find(5)->5];
		//3 done
		fa[3]=2;
	//2 done
	fa[2]=5;
	
//5 done

代码实现:

/*375ms,6780KB*/

#include<cstdio>
#include<cstring>
const int mx = 905;

int sum[mx], son[mx][mx], query[mx][mx], fa[mx];
bool vis[mx], hasroot[mx];

int find(int x) {return fa[x] == x ? x : fa[x] = find(fa[x]);}

void tarjan(int u)
{
	vis[u] = true;
	int i, v;
	for (i = 1; i <= query[u][0]; ++i)
	{
		v = query[u][i];///要想使sum增加,v必须是先前访问过的点
		if (vis[v]) ++sum[find(v)];
	}
	for (i = 1; i <= son[u][0]; ++i)
	{
		v = son[u][i];
		if (!vis[v]) tarjan(v), fa[v] = u;
	}
}

inline void add(int a, int b)
{
	++query[a][0];///我们用query[节点a][0]表示查询LCA(a,?)的个数
	query[a][query[a][0]] = b;///记录这一查询LCA(a,b)
}

int main()
{
	int n, i, j, k, Q;
	while (~scanf("%d", &n))
	{
		memset(vis, 0, sizeof(vis));
		memset(sum, 0, sizeof(sum));
		memset(hasroot, 0, sizeof(hasroot));
		for (i = 1; i <= n; ++i)
		{
			fa[i] = i, query[i][0] = 0;
			scanf("%d", &j);
			while (getchar() != '(');
			scanf("%d", &son[j][0]);///注意,我们用son[节点编号][0]表示儿子个数
			while (getchar() != ')');
			for (k = 1; k <= son[j][0]; ++k)
			{
				scanf("%d", &son[j][k]);///记录此节点的儿子节点编号
				hasroot[son[j][k]] = true;
			}
		}
		scanf("%d", &Q);
		while (Q--)
		{
			while (getchar() != '(');
			scanf("%d%d", &i, &j);
			while (getchar() != ')');
			add(i, j), add(j, i);///双向添加查询
		}
		for (i = 1; i <= n; ++i)
			if (!hasroot[i]) {tarjan(i); break;}///从根节点进入
		for (i = 1; i <= n; ++i)
			if (sum[i]) printf("%d:%d\n", i, sum[i]);
	}
	return 0;
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 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、付费专栏及课程。

余额充值