利用Tarjan算法(LCA)求树上任意两点的距离

需求

给出 n 个点的一棵树 (无向边)

多次询问树上两点之间的最短距离

点的编号是1-n

思路

分析题意

设两个点分别为 x y

两个点到根节点的距离预处理到数组中 用 d[x] d[y] 表示

分析发现 树上两个点的距离可以用公式表示为

d[x] + d[y] - 2 * d[ LCA(x,y) ]

其中LCA(x,y) 表示点x和点y的最近公共祖先

红色路径表示d[x]  绿色路径表示d[y]

Tarjin算法求最近公共祖先(LCA)

基于深度优先遍历 把所有点分为三大类

第一类是已经被遍历过且回溯的点 在当前路径的左侧 绿色

第二类是正在搜索的点 从根节点到当前节点的路径 红色

第三类是还未搜索到的点 在当前路径的右侧 蓝色

思考

我们发现 图中被绿色线条框住的三个点(1,2,3)

点x的最近公共祖先是相同的点(在图中已经标识为LCA

所以 我们将(1,2,3)这三个点看作一个小子树 (利用并查集)

并用LCA标识点代表这个子树

以后的查询若涉及该子树中的点

答案即为LCA标识点

算法步骤

对于当前遍历的这个点(设为x)

查询与点x相关的所有询问

如果询问中的另一个点y(ex.询问为求点x与点y的距离处于绿色部分 也就是第一类

那么最近公共祖先就是点y所在并查集中的代表元素

对于已经遍历过的小子树(第一类)

可以用当前路径上(第二类)的某一个点代表它

AC代码(含详细注释)

#include<cstring>
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
typedef pair<int, int>PII;
const int N = 2e4 + 10;
const int M = N * 2;//无向边 边数两倍
int h[N], e[M], w[M], ne[M], idx;
int dist[N];//每个点与根节点的距离
int p[N];
int st[N];
int n, m;
int res[N];//存每一次询问的结果
vector<PII>query[N];// first存查询的另外一个点 second存查询编号

void add(int a, int b, int c)
{
	e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

void dfs(int u, int fa)
{
	for (int i = h[u]; ~i; i = ne[i])
	{
		//当前到达的时点j
		int j = e[i];
		//树上DFS确保搜索的单向性
		//只从上往下搜索
		if (j == fa)continue;
		dist[j] = dist[u] + w[i];
		dfs(j, u);
	}
}

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

//在线算法:读一个询问,处理一个,输出一个
//离线算法:读完全部询问,再全部处理完,再全部输出
//O(n+m)离线求LCA
void tarjan(int u)
{
	//2表示该点处于当前路径
	st[u] = 2;
	for (int i = h[u]; ~i; i = ne[i])
	{
		int j = e[i];
		if (!st[j])
		{
			tarjan(j);
			//并查集的合并 形成小子树
			p[j] = u;
		}
	}

	for (auto it : query[u])
	{
		int y = it.first, id = it.second;
		if (st[y] == 1)//如果询问的另一个点已经被遍历过了
		{
			//获取其所在并查集的代表元素 即为LCA点
			int anc = find(y);
			res[id] = dist[u] + dist[y] - dist[anc] * 2;
		}
	}

	//1表示该点处于第一类点 已经搜索完毕且回溯
	st[u] = 1;
}

int main()
{
	//初始化邻接表头节点
	memset(h, -1, sizeof h);

	scanf("%d%d", &n, &m);

	//存树
	for (int i = 0; i < n - 1; i++)
	{
		int a, b, c;
		scanf("%d%d%d", &a, &b, &c);
		add(a, b, c), add(b, a, c);
	}

	//存询问
	for (int i = 0; i < m; i++)
	{
		int a, b;
		scanf("%d%d", &a, &b);
		//对于点a我们要查询与点b之间的距离
		//这是第i次询问
		if (a != b)//相同的两个点距离为0
		{
			query[a].push_back({ b,i });
			//要查询的另一个点可能不在第一类点
			//所以要双向记录查询要求
			//当另一个点作为"当前遍历点"时 可以查到答案
			query[b].push_back({ a,i });
		}
	}

	//并查集初始化
	for (int i = 1; i <= n; i++)p[i] = i;

	//DFS预处理每一个点到根节点的距离 dist数组
	dfs(1, -1);

	//tarjan算法也是一种DFS
	tarjan(1);

	for (int i = 0; i < m; i++)printf("%d\n", res[i]);

	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

没伞的男孩

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值