题解:AcWing 1171 距离

【题目来源】

AcWing:1171 距离 - AcWing题库

【题目描述】

给出 \(n\) 个点的一棵树,多次询问两点之间的最短距离。

注意:

  • 边是无向的。
  • 所有节点的编号是 \(1,2,\dots,n\)

【输入】

第一行为两个整数 \(n\)\(m\)\(n\) 表示点数,\(m\) 表示询问次数;

下来 \(n-1\) 行,每行三个整数 \(x,y,k\),表示点 \(x\) 和点 \(y\) 之间存在一条边长度为 \(k\)

再接下来 \(m\) 行,每行两个整数 \(x,y\),表示询问点 \(x\) 到点 \(y\) 的最短距离。

树中结点编号从 \(1\)\(n\)

【输出】

\(m\) 行,对于每次询问,输出一行询问结果。

【输入样例】

2 2
1 2 100
1 2
2 1

【输出样例】

100
100

【核心思想】

  1. 问题分析:给定一棵包含 \(n\) 个节点的带权树,边是无向的,有 \(m\) 个询问,每个询问给出节点对 \((x, y)\),需要求两点之间的最短距离。这是一个树上距离查询问题,可以利用最近公共祖先(LCA)的性质求解:\(dist(x, y) = dist(x, root) + dist(y, root) - 2 \times dist(LCA(x, y), root)\)

  2. 算法选择

    • Tarjan 离线 LCA 算法:在 DFS 过程中利用并查集维护当前搜索路径上的祖先信息,同时处理与当前节点相关的所有询问
    • DFS 预处理距离:从根节点出发进行 DFS,计算每个节点到根节点的距离 \(dist[]\)
    • 并查集维护祖先\(p[x]\) 表示节点 \(x\) 在当前搜索路径上的祖先(即 DFS 树中的父节点)
    • 离线处理询问:将所有询问保存下来,在 DFS 过程中当两个节点都访问过时即可计算答案
  3. 关键步骤

    • 建树:读入 \(n-1\) 条边 \((x, y, k)\),使用邻接表存储带权树
    • 保存询问:对于每个询问 \((a, b)\),将 \((b, id)\) 加入 \(query[a]\),将 \((a, id)\) 加入 \(query[b]\)\(id\) 为询问编号)
    • DFS 预处理距离:从根节点(节点 \(1\))开始 DFS,计算 \(dist[u]\) 表示 \(u\) 到根的距离:dist[j] = dist[u] + w
    • Tarjan LCA(DFS 过程中):
      • 标记 \(st[u] = 1\)(正在访问)
      • 递归访问所有未访问的子节点 \(j\),访问完后设置 \(p[j] = u\)(并查集合并)
      • 标记 \(st[u] = 2\)(已访问且回溯)
      • 处理询问:遍历 \(query[u]\) 中所有与 \(u\) 相关的询问 \((y, id)\)
        • \(st[y] == 2\)\(y\) 已访问且回溯),则 \(LCA(u, y) = find(y)\)
        • 计算答案:\(res[id] = dist[u] + dist[y] - 2 \times dist[LCA]\)
    • 输出结果:按询问编号输出所有答案
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n + m)\),DFS 遍历 \(O(n)\),并查集操作近似 \(O(1)\),处理每个询问 \(O(1)\)
    • 空间复杂度:\(O(n + m)\),邻接表 \(O(n)\),询问存储 \(O(m)\),并查集、距离数组、标记数组各 \(O(n)\)
    • 适用于离线查询场景(所有询问已知)
  5. Tarjan 离线 LCA 的核心思想

    • 离线处理:一次性读入所有询问,在 DFS 过程中统一处理
    • 并查集维护祖先\(p[x]\) 始终指向 \(x\) 在当前 DFS 路径上的祖先,利用并查集路径压缩快速查询
    • 访问状态标记\(st[u] = 0/1/2\) 分别表示未访问/正在访问/已访问且回溯,只有已回溯的节点才能确定 LCA
    • LCA 判定:当处理节点 \(u\) 的询问 \((u, y)\) 时,若 \(y\) 已回溯,则 \(LCA(u, y) = find(y)\),即 \(y\) 在当前 DFS 路径上的祖先
    • 树上距离公式:利用 LCA 将两点距离转化为到根距离的组合:\(dist(x, y) = dist(x) + dist(y) - 2 \times dist(LCA)\)
    • 适用场景:离线查询、需要批量处理多个 LCA 询问的问题

【解题思路】

【算法标签】

最近公共祖先

【代码详解】

#include <bits/stdc++.h>
using namespace std;
const int N=100005, M=N*2, INF=1e9;
typedef pair<int, int> PII;
int n, m;  //n节点数,m询问数
struct Edge {
    int b, w;  //点a到点b有边,权值w
};
vector<Edge> g[N];  //图的邻接表表示,g[i]表示点i到b有边,权值w

int p[N];  //p[y]:点y的根
int res[N];  //res[i] 第i个询问结果
int dist[N];  //每个点和1号点的距离
int st[N];  //节点DFS访问类型 2-访问且回溯 1-当前访问 0-没访问
//query[i][first][second]  存查询当前节点i的另一个点是first,second存查询编号
vector<PII> query[N];  //保存查询信息

//求每个点和1号点(根节点)的距离dist,深搜。u, fa当前节点及其父节点
void dfs(int u, int fa)
{
    for (int i=0; i<g[u].size(); i++) {  //遍历u的儿子j
        Edge nd=g[u][i];
        int j=nd.b, dis=nd.w;
        if (j==fa) continue;  //u的邻接点是其父节点
        dist[j]=dist[u]+dis;
        dfs(j,u);  //深搜求其他节点到根的距离
    }
}

int find(int x)  //找x的根
{
    if (p[x]!=x) p[x] = find(p[x]);
    return p[x];
}

//Tarjan求LCA,u当前节点,从根节点开始深搜
void Tarjan(int u)
{
    st[u]=1;  //正在搜索的类型 1
    for (int i=0; i<g[u].size(); i++) {  //遍历u的儿子j
        Edge nd = g[u][i];
        int j=nd.b, dis=nd.w;
        if (st[j]==0) {  //点j没被访问,深搜访问
            Tarjan(j);
            p[j]=u;  //指定儿子j的父亲是u
        }
    }
    st[u]=2;  //u为根的子树访问结束,再回溯到u,标记节点u已经访问+回溯
    for (int i=0; i<query[u].size(); i++) {  //与当前节点相关的询问
        PII item = query[u][i];
        int y = item.first;  //当前节点u的另外一个点y,second存查询编号
        int id = item.second;
        if (st[y]==2) {  //另外一个点y已经访问且回溯
            int ans = find(y);  //前结点u的另外一个点y的LCA
            res[id] = dist[u]+dist[y]-2*dist[ans];
        }
    }
}
int main()
{
    cin >> n >> m;  //n节点数,m询问次数
    for (int i=1; i<n; i++) {  //n-1条边
        int a, b, c;
        cin >> a >> b >> c;
        g[a].push_back({b,c});  //建邻接表,点a到点b有边,权值1
        g[b].push_back({a,c});  //建邻接表
    }
    for (int i=1; i<=m; i++) {  //保存查询信息
        int a, b;
        cin >> a >> b;
        if (a!=b) {  //如果a=b距离就是0,不处理
            //query[a][first][second] 存查询当前节点a的另外一个点是b,第i个查询
            query[a].push_back({b,i});
            query[b].push_back({a,i});
        }
    }
    for (int i=1; i<n; i++) p[i]=i;  //初始化并查集每个父节点是它自己
    dfs(1, -1);  //求每个点和点1(根节点)的距离dist;<当前节点,其父节点>
    Tarjan(1);  //Tarjan求LCA,深搜点1(根节点)
    for (int i=1; i<=m; i++)
        cout << res[i] << endl;
    return 0;
}
// 使用链式前向星再写一遍
#include <bits/stdc++.h>
using namespace std;
const int N=20010, M=N*2, INF=1e9;
typedef pair<int, int> PII;
int n, m;  //n节点数,m询问数
int h[N], e[M], w[M], ne[M], idx;  //邻接表
int p[N];  //p[y]:点y的根且该根在当前搜索路径上的点
int res[N];  //res[i] 第i个询问结果
int dist[N];  //每个点和1号点的距离
int st[N];  //节点DFS访问类型 2-访问且回溯 1-当前访问 0-没访问
//query[i][first][second]  存查询当前节点i的另一个点是first,second存查询编号
vector<PII> query[N];  //保存查询信息

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

//求每个点和1号点(根节点)的距离dist,深搜。u, fa当前节点及其父节点
void dfs(int u, int fa)
{
    for (int i=h[u]; i!=-1; i=ne[i]) {  //遍历u的儿子j
        int j=e[i];
        if (j==fa) continue;  //u的邻接点是其父节点
        dist[j]=dist[u]+w[i];
        dfs(j,u);  //深搜求其他节点到根的距离
    }
}

int find(int x)  //找x的根
{
    if (p[x]!=x) p[x] = find(p[x]);
    return p[x];
}

//Tarjan求LCA,u当前节点,从根节点开始深搜
void Tarjan(int u)
{
    st[u]=1;  //正在搜索的类型 1
    for (int i=h[u]; i!=-1; i=ne[i]) {  //遍历u的儿子j
        int j=e[i];
        if (st[j]==0) {  //点j没被访问,深搜访问
            Tarjan(j);
            p[j]=u;  //指定儿子j的父亲是u
        }
    }
    st[u]=2;  //u为根的子树访问结束,再回溯到u,标记节点u已经访问+回溯
    for (int i=0; i<query[u].size(); i++) {  //与当前节点相关的询问
        PII item = query[u][i];
        int y = item.first;  //当前节点u的另外一个点y,second存查询编号
        int id = item.second;
        if (st[y]==2) {  //另外一个点y已经访问且回溯
            int ans = find(y);  //前结点u的另外一个点y的LCA
            res[id] = dist[u]+dist[y]-2*dist[ans];
        }
    }
}
int main()
{
    cin >> n >> m;  //n节点数,m询问次数
    memset(h, -1, sizeof(h));
    for (int i=1; i<n; i++) {  //n-1条边
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c), add(b, a, c);  //建邻接表
    }
    for (int i=1; i<=m; i++) {  //保存查询信息
        int a, b;
        cin >> a >> b;
        if (a!=b) {  //如果a=b距离就是0,不处理
            //query[a][first][second] 存查询当前节点a的另外一个点是b,第i个查询
            query[a].push_back({b,i});
            query[b].push_back({a,i});
        }
    }
    for (int i=1; i<n; i++) p[i]=i;  //初始化并查集每个父节点是它自己
    dfs(1, -1);  //求每个点和点1(根节点)的距离dist;<当前节点,其父节点>
    Tarjan(1);  //Tarjan求LCA,深搜点1(根节点)
    for (int i=1; i<=m; i++)
        cout << res[i] << endl;
    return 0;
}

【运行结果】

2 2
1 2 100
1 2
2 1
100
100
posted @ 2026-06-16 10:13  团爸讲算法  阅读(5)  评论(0)    收藏  举报