【题目描述】
给出 n 个点的一棵树,多次询问两点之间的最短距离。
注意:边是双向的。
【输入】
第一行为两个整数 n 和 m。n 表示点数,m 表示询问次数;
下来 n−1 行,每行三个整数 x,y,k,表示点 x 和点 y 之间存在一条边长度为 k;
再接下来 m 行,每行两个整数 x,y,表示询问点 x 到点 y 的最短距离。
【输出】
输出 m 行。对于每次询问,输出一行。
【输入样例】
2 2
1 2 100
1 2
2 1
【输出样例】
100
100
【提示】
样例输入 2
3 2
1 2 10
3 1 15
1 2
3 2
样例输出 2
10
25
数据范围与提示:
对于全部数据,2≤n≤104,1≤m≤2×104,0<k≤100,1≤x,y≤n。
分析
- 找LCA的过程和上题一致:1557:祖孙询问,代码和上题差不多,只不过多加一个距离问题,这里解释下,如何求两点之间的最短距离;
- 用一个数组dist存每个点到根节点的距离,通过下图发现,任意两点的距离都可转化为,他们各自到根节点的距离和 减去 2倍最近公共祖先到根节点的距离(重叠了两次);
- 可以采用 倍增在线求LCA、Tarjan离线求LCA;
- 在线做法:输入一个询问,处理一个,输出一个;离线做法:将全部询问存起来,再处理完这全部的询问,再全部输出;
倍增在线求LCA
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 20010, M = N * 2;
int n, m;
int h[N], e[M], w[M], ne[M], idx;
int q[N];
//fa[i,j]表示从i向上走 2^j 步能到的点;depth表示深度
int depth[N], fa[N][21];// 2^16 >40010
int dist[N];//表示每个点到根节点的距离
void add(int a, int b, int c) {
e[++idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx;
}
//初始化depth、fa
void bfs() {
memset(depth, 0x3f, sizeof depth);
//哨兵:depth[0] = 0,f[i,j]默认就是0了
depth[0] = 0;
int hh = 0, tt = 0;
//第一个点——根节点
q[0] = 1;
depth[1] = 1;
while (hh <= tt) {
int u = q[hh++];
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
//如果u,v相差不是一层,说明v还没被搜过,加队列
if (depth[v] > depth[u] + 1) {
depth[v] = depth[u] + 1;
q[++tt] = v;
//初始化fa
fa[v][0] = u;
for (int k = 1; k <= 20; ++k) {
//先跳2^(k-1)步到f[v][k-1],再跳2^(k-1)步
fa[v][k] = fa[fa[v][k - 1]][k - 1];
}
//设置dist
dist[v] = dist[u] + w[i];
}
}
}
}
//求两个点的公共祖先
int lca(int a, int b) {
//a要在b下面
if (depth[a] < depth[b])
swap(a, b);
//1. 先跳到同一层(一个数肯定能通过二进制表示,这层for结束就到了同一层)
for (int k = 20; k >= 0; --k) {
//a先向上跳2^k步
//不用怕k=16或者20起始值太大,有哨兵,不怕他第一次跳太远,导致直接跳出根节点,如果跳出根节点,f[i][j]=0会出手的
//哨兵的作用:如果f[a][k]跳出了根节点,那么左边就是depth[0]=0,自然不满足当前if,i--找下一个i
if (depth[fa[a][k]] >= depth[b]) {
//a赋上新位置,继续往上跳
a = fa[a][k];
}
}
//找到公共祖先
if (a == b)
return a;
//2. 同时往上跳,至到他们最近公共祖先的下一层
for (int k = 20; k >= 0; --k) {
//说明还没跳到公共祖先
//哨兵的作用:如果a跳出去了,b肯定也跳出去了,因为同一层开始跳的,然后fa[a][k]=0,自然不满足当前的if,i--
if (fa[a][k] != fa[b][k]) {
//更新这次跳到的新位置
a = fa[a][k];
b = fa[b][k];
}
}
//当前在公共祖先的下一层,再往上跳一层即可
return fa[a][0];//fa[b][0]也是可以的
}
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);//无向边
}
//从任意一点当根节点开搜即可
bfs();
while (m--) {
int x, y;
scanf("%d%d", &x, &y);
int p = lca(x, y);
printf("%d\n", dist[x] + dist[y] - 2 * dist[p]);
}
return 0;
}
Tarjan离线求LCA
这个方法是通过标记的方法求LCA,但是求距离和上面一样,求公共祖先的方式不一样;使用dfs初始化dist,从上往下搜素;
所有节点分为三种状态:
2 已经遍历过,且回溯过的点
1 代表正在搜的分支
0 还没有搜到的点
#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII;
const int N = 20010, M = N * 2;
int n, m;
int h[N], e[M], w[M], ne[M], idx;
int st[N];
int dist[N];//表示每个点到根节点的距离
int p[N]; //并查集
int ans[N];//每一组询问的答案
vector<PII> v[N]; //v[u][v][id]: first存u的邻边v,second存查询编号id
void add(int a, int b, int c) {
e[++idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx;
}
//初始化dist
void dfs(int u, int father) {
for (int i = h[u]; ~i; i = ne[i]) {
// u->v
int v = e[i];
//不往上搜
if (v == father)
continue;
//父节点+这条边
dist[v] = dist[u] + w[i];
dfs(v, u);
}
}
int find(int x) {
if (p[x] == x)
return x;
return p[x] = find(p[x]);
}
//所有节点分为三种状态:
//2 已经遍历过,且回溯过的点
//1 代表正在搜的分支
//0 还没有搜到的点
void tarjan(int u) {
//首先标记为正在搜u这个分支
st[u] = 1;
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
//没被遍历过
if (!st[v]) {
tarjan(v);
//回溯时进行,这句话不能写上面
p[v] = u; //把v合并到u
}
}
//和u相关的查询
for (auto item: v[u]) {
int v = item.first, id = item.second;
if (st[v] == 2) {
int anc = find(v);//u,v的公共祖先
ans[id] = dist[u] + dist[v] - 2 * dist[anc];
}
}
//u已经遍历过且回溯过
st[u] = 2;
}
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 x, y;
scanf("%d%d", &x, &y);
//x=y,ans数组这个查询编号id位置的值默认就是0
if (x != y) {
v[x].push_back({y, i});
v[y].push_back({x, i});
}
}
//初始化p数组
for (int i = 1; i <= n; i++)
p[i] = i;
//从任意一点开搜即可
dfs(1, -1);
tarjan(1);
for (int i = 0; i < m; ++i) {
cout << ans[i] << endl;
}
return 0;
}