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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值