需求
给出 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;
}