题解:洛谷 P10289 [GESP样题 八级] 小杨的旅游

【题目来源】

洛谷:[P10289 GESP样题 八级] 小杨的旅游 - 洛谷

【题目描述】

小杨准备前往 B 国旅游。

B 国有 \(n\) 座城市,这 \(n\) 座城市依次以 \(1\)\(n\) 编号。城市之间由 \(n-1\) 条双向道路连接,任意两座城市之间均可达(即任意两座城市之间存在可达的路径)。

小杨可以通过双向道路在城市之间移动,通过一条双向道路需要 \(1\) 单位时间。

B 国城市中有 \(k\) 座城市设有传送门。设有传送门的城市的编号依次为 \(b_1,b_2, \cdots ,b_k\)。小杨可以从任意一座设有传送门的城市花费 \(0\) 单位时间前往另一座设有传送门的城市。

注:如果两座设有传送门的城市之间存在双向道路,那么小杨可以选择通过双向道路移动,也可以选择通过传送门传送。

小杨计划在 B 国旅游 \(q\) 次。第 \(i\) 次旅游(\(1 \le i \le q\)),⼩杨计划从编号为 \(u_i\) 的城市前往编号为 \(v_i\) 的城市,小杨希望你能求出所需要的最短时间。

【输入】

第一行包含三个正整数 \(n,k,q\),分别表示 B 国的城市数量,设有传送门的城市数量,以及小杨计划在 B 国旅游的次数。
接下来 \(n-1\) 行,每行包含两个正整数 \(x_i, y_i\),表示一条双向道路连接的两座城市的编号。
\(n + 1\) 行包含 \(k\) 个正整数,表示设有传送门的城市的编号。
接下来 \(q\) 行,每行包含两个正整数 \(u_i,v_i\),表示小杨第 \(i\) 次旅游行程的起点城市编号与终点城市编号。

【输出】

输出共 \(q\) 行。第 \(i\) 行(\(1 \leq i \leq q\))输出一个非负整数,表示小杨计划第 \(i\) 次旅游从编号为 \(u_i\) 的城市前往编号为 \(v_i\) 的城市所需要的最短时间。

【输入样例】

7 2 1
5 7
3 6
2 3
1 5
5 4
1 2
7 4
3 7

【输出样例】

4

【算法标签】

《洛谷 P10289 小杨的旅游》 #广度优先搜索BFS# #最短路# #最近公共祖先LCA# #GESP#

【代码详解】

#include <bits/stdc++.h>
using namespace std;
const int N = 200005, M = N * 2;  // N: 最大节点数, M: 最大边数(无向图每条边存两次)
typedef pair<int, int> PII;  // 定义pair类型,用于存储节点和步数
int n, k, Q;  // n: 节点数, k: 特殊节点数, Q: 查询次数
int h[N], e[M], ne[M], idx;  // 邻接表存储树
int dep[N], dist[N], fa[N][30];  // dep: 节点深度, dist: 到最近特殊节点的距离, fa: 倍增祖先表
bool st[N];  // 标记数组,用于BFS2
queue<int> q;  // BFS队列
queue<PII> q2;  // 多源BFS队列

// 添加无向边
void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;  // 添加边a->b
}

// 预处理节点深度和倍增祖先表
void bfs(int root)
{
    memset(dep, 0x3f, sizeof(dep));  // 初始化为无穷大(0x3f3f3f3f)
    dep[0] = 0, dep[root] = 1;  // 虚拟0节点深度为0, 根节点深度为1
    q.push(root);
    
    while (!q.empty())  // 计算节点深度
	{  
        int t = q.front();  
        q.pop();
        
        for (int i = h[t]; i != -1; i = ne[i])  // 遍历节点t的所有邻接点
		{
            int j = e[i];  // 邻接节点j
            
            if (dep[j] > dep[t] + 1)  // 如果j的深度还未计算或计算值不是最优
			{  
                dep[j] = dep[t] + 1;  // 计算j的深度
                q.push(j);  // j入队
                fa[j][0] = t;  // j向上走2^0=1步即为父节点t
                
                for (int k = 1; k <= 29; k++)  // 递推计算fa[j][k]
				{  
                    // j向上走2^k步等于j向上走2^(k-1)步后, 再向上走2^(k-1)步
                    fa[j][k] = fa[fa[j][k-1]][k-1];
                }
            }
        }
    }
}

// LCA倍增算法,计算节点x和节点y的最近公共祖先
int lca(int x, int y)  
{
    if (dep[x] < dep[y]) swap(x, y);  // 保证x为深度较大的节点
    
    // 步骤1:把节点x向上跳,直到与节点y深度相同
    for (int k = 29; k >= 0; k--)  // 从高位向低位尝试
        if (dep[fa[x][k]] >= dep[y])  // 如果跳2^k步后深度仍大于等于y
            x = fa[x][k];  // 就跳
    
    if (x == y) return x;  // 如果x和y已经相同, 则找到LCA

    // 步骤2:两个节点同时向上跳,跳到公共祖先的下一层
    for (int k = 29; k >= 0; k--) 
	{
        if (fa[x][k] != fa[y][k])  // 如果跳2^k步后祖先不同, 就都跳上去
		{  
            x = fa[x][k];
            y = fa[y][k];
        }
    }
    return fa[x][0];  // 返回x或y的父节点, 即为最近公共祖先
}   

// 多源BFS,计算每个节点到最近特殊节点的距离
void bfs2()
{
    while (!q2.empty())
    {
        int u = q2.front().first;  // 当前节点
        int step = q2.front().second;  // 到最近特殊节点的距离
        // cout << "u step " << u << " " << step << endl;
        q2.pop();
        
        for (int i = h[u]; i != -1; i = ne[i])  // 遍历u的所有邻接节点
        {
            int j = e[i];  // 邻接节点j
            
            if (st[j]) continue;  // 如果j已访问,跳过
            
            dist[j] = step + 1;  // 更新j到最近特殊节点的距离
            q2.push({j, step + 1});  // j入队
            st[j] = 1;  // 标记j已访问
        }
    }
}

int main()
{
    cin >> n >> k >> Q;  // 输入节点数、特殊节点数、查询次数
    memset(h, -1, sizeof(h));  // 初始化邻接表
    
    // 输入n-1条边,构建树
    for (int i = 1; i < n; i++)   
    {
        int x, y; 
        cin >> x >> y;
        add(x, y), add(y, x);  // 添加无向边
    }
    
    // 初始化特殊节点
    for (int i = 1; i <= k; i++)
    {
        int x; 
        cin >> x;  // 输入特殊节点编号
        dist[x] = 0;  // 特殊节点到自身的距离为0
        q2.push({x, 0});  // 特殊节点入队
        st[x] = 1;  // 标记特殊节点已访问
    }
    
    bfs(1);  // 从节点1开始BFS,预处理深度和祖先表
    
    bfs2();  // 多源BFS,计算每个节点到最近特殊节点的距离
    
    // 处理Q个查询
    while (Q--)
    {
        int x, y; 
        cin >> x >> y;  // 输入查询的两个节点
        
        int tmp = lca(x, y);  // 计算x和y的最近公共祖先
        int ans;
        
        if (k != 0)  // 如果有特殊节点
            // 两种走法的较小值:
            // 1. 直接走树边:x到y的树上距离 = dep[x] + dep[y] - 2*dep[tmp]
            // 2. 通过特殊节点:x到最近特殊节点的距离 + y到最近特殊节点的距离
            ans = min(dep[x] - dep[tmp] + dep[y] - dep[tmp], dist[x] + dist[y]);
        else  // 如果没有特殊节点,只能走树边
            ans = dep[x] - dep[tmp] + dep[y] - dep[tmp];
            
        cout << ans << endl;  // 输出结果
    }
    
    return 0;
}

【运行结果】

7 2 1
5 7
3 6
2 3
1 5
5 4
1 2
7 4
3 7
4
posted @ 2026-03-14 21:38  团爸讲算法  阅读(3)  评论(0)    收藏  举报