P9245 [蓝桥杯 2023 省 B] 景区导游 题解

题目解析

本题要求在给定的树结构和一个节点访问序列中,对于每个节点,计算跳过该节点后的路径总长度。关键在于高效计算树上任意两点间距离,并利用预处理优化计算过程。


方法思路

  • 树结构预处理

DFS 计算节点到根的距离:通过遍历树,记录每个节点到根节点的距离,存储于 dist 数组。

倍增法预处理 LCA:使用二进制跳跃法预处理每个节点的祖先信息,以便快速查询任意两节点的最近公共祖先。

  • 路径距离计算

任意两节点 u 和 v 的距离公式:

dist(u, v) = dist[u] + dist[v] - 2 * dist[LCA(u, v)]

预处理原路径总长度:遍历访问序列,累加每对相邻节点的距离。

  • 高效处理每个跳过点

端点处理:若跳过首节点或尾节点,直接减去对应的单段距离。

中间点处理:减去原两段距离,并加上新段距离,避免重复计算。


解决方案

算法:倍增求最近公共祖先

时间复杂度:

  • 初始化 O(n log n)
  • 求最短距离 O(m log n)

C++ 代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
const int N=1e5+10,M=N*2;

// 链式前向星存储图的相关数组
int e[M],ne[M],h[N],idx;
ll w[M];

//  倍增求LCA所需数组
int depth[N],fa[N][20];//log(1e5)约等于17,开到20
int q[N];

int n,m;
ll dist[N];// 从根节点到每个节点的距离数组
int order[N];

void add(int a,int b,ll 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]){// 遍历当前节点 u 的所有邻接节点
		int j=e[i];
		if(j!=fa){// 如果邻接节点 j 不是当前节点 u 的父节点
			dist[j]=dist[u]+w[i];// 计算邻接节点 j 到根节点的距离
			dfs(j,u);// 递归调用 dfs 函数,继续搜索邻接节点 j
		}
	}
}

// 广度优先搜索函数,用于计算每个节点的深度和祖先信息
void bfs(int root){
    memset(depth,0x3f,sizeof depth);
    depth[0]=0,depth[root]=1;// 虚拟节点 0 的深度为 0,根节点的深度为 1
    int hh=0,tt=1;
    q[0]=root;
    while(hh<tt){
        int u=q[hh++];
        for(int i=h[u];~i;i=ne[i]){
            int v=e[i];
            if(depth[v]>depth[u]+1){
                depth[v]=depth[u]+1;// 如果邻接节点 v 的深度大于当前节点 u 的深度加 1就更新邻接节点 v 的深度
                q[tt++]=v;
                fa[v][0]=u;// 记录邻接节点 v 的父节点
                for(int k=1;k<=17;k++) fa[v][k]=fa[fa[v][k-1]][k-1];// 预处理祖先信息
            }
        }
    }
}

// 计算两个节点 a 和 b 的最近公共祖先
int lca(int a,int b){
    if(depth[a]<depth[b]) swap(a,b);// 确保 a 的深度不小于 b 的深度
    for(int k=17;k>=0;k--) if(depth[fa[a][k]]>=depth[b]) a=fa[a][k];// 让 a 跳到与 b 同一深度
    if(a==b) return a;// 如果 a 和 b 相等,说明 a 就是最近公共祖先
    
    // 同时向上跳,直到找到最近公共祖先
    for(int k=17;k>=0;k--){
        if(fa[a][k]!=fa[b][k]){
            a=fa[a][k],b=fa[b][k];
        }
    }
    
    return fa[a][0];
}

// 计算两个节点 a 和 b 之间的距离
ll get_dist(int a,int b){
	return dist[a]+dist[b]-2*dist[lca(a,b)];
}

int main(){
    scanf("%d%d",&n,&m);
    memset(h,-1,sizeof h);// 初始化链式前向星头数组为 -1
    
    for(int i=0,a,b;i<n-1;i++){
    	long long c;
        scanf("%d%d%lld",&a,&b,&c);
        add(a,b,c),add(b,a,c);// 添加无向边
    }

    int root=1;
    dist[root]=0;// 初始化根节点到自身的距离为 0
    dfs(root,-1);// 调用 dfs 函数计算到树根的距离
    bfs(root);// 调用 bfs 函数计算深度和祖先信息
    
    for(int i=1;i<=m;i++) scanf("%d",&order[i]);
    
    ll original=0;
    for(int i=1;i<=m-1;i++) original+=get_dist(order[i],order[i+1]);// 计算原始路径的总距离
    
    // 遍历每个跳过的点
    for(int i=1;i<=m;i++){
    	ll ans=0;
    	if(i==1) ans=original-get_dist(order[1],order[2]);// 如果是第一个查询点
		else if(i==m) ans=original-get_dist(order[m-1],order[m]);// 如果是最后一个查询点
		else{
			ll part1=get_dist(order[i-1],order[i]),part2=get_dist(order[i],order[i+1]);// 计算删除该点前的两段路径长度
			ll new_part=get_dist(order[i-1],order[i+1]);// 计算删除该点后的新路径长度
			ans=original-part1-part2+new_part;// 计算删除该点后的总距离
		}
		printf("%lld ",ans);
	}
    return 0;
}

代码解释

  • 数据结构与输入处理

使用邻接表存储树结构,add 函数处理双向边。

读取节点访问序列并存储于 order 数组。

  • 预处理阶段

DFS:从根节点出发,计算每个节点到根的距离。

BFS:构建每个节点的深度和倍增表,用于快速查询最近公共祖先。

  • 距离计算

get_dist 函数利用 LCA 计算两点间距离,公式为两节点到根的距离减去两倍 LCA 到根的距离。

  • 路径总和优化

预处理原路径总长度后,针对每个跳过点,通过 \(O(1)\) 时间复杂度计算新路径长度,避免重复遍历。


后记

十年OI一场空,不开long long见祖宗!

posted @ 2025-05-05 11:04  九三青梧  阅读(29)  评论(0)    收藏  举报