把每台电脑的度数看成点权,询问就是树上两点路径点权和;预处理根到每个点的前缀和,再用 LCA 把路径拆成两段即可。
OJ: luogu
题目 ID: P8805
难度:普及+/提高
标签:LCA倍增树形结构
日期: 2026-06-20 02:37
题意
给一棵
如果一台电脑直接连了
查询很多次:
- 从电脑
向电脑 发送信息 - 最短时间是多少
因为原图是一棵树,所以
- 这条路径上所有点的延迟之和
这里发送点和接收点也要算一次;如果
样例树
样例树结构如下:
graph G {
1 -- 2;
1 -- 3;
2 -- 4;
}
各点度数分别是:
比如查询 2 -> 3,路径是 2-1-3,答案就是:
思路
先看一个最直接的小数据暴力:
cpp
// brute.cpp:每次查询直接在树上找唯一路径,把路径上的点度数加起来。
// 这个做法复杂度较高,只适合小数据理解题意和对拍。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 25;
int n, m;
vector<int> g[MAXN];
int deg[MAXN];
int parent_arr[MAXN];
bool vis[MAXN];
bool dfs_find(int u, int target, int fa) {
if (u == target) {
return true;
}
for (size_t i = 0; i < g[u].size(); i++) {
int v = g[u][i];
if (v == fa) {
continue;
}
parent_arr[v] = u;
if (dfs_find(v, target, u)) {
return true;
}
}
return false;
}
long long query_path_sum(int u, int v) {
for (int i = 1; i <= n; i++) {
parent_arr[i] = 0;
}
dfs_find(u, v, 0);
long long ans = 0;
int x = v;
while (x != u) {
ans += deg[x];
x = parent_arr[x];
}
ans += deg[u];
return ans;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
g[i].clear();
deg[i] = 0;
}
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
deg[u]++;
deg[v]++;
}
while (m--) {
int u, v;
cin >> u >> v;
cout << query_path_sum(u, v) << '\n';
}
return 0;
}暴力做法就是:
- 每次查询在树上找出
到 的唯一路径 - 把路径上所有点的度数累加起来
这个方法很直观,但查询多的时候,每次都重新找路径会慢。
关键观察是:
- 题目本质上就是“树上路径点权和”
- 每个点的点权固定等于它的度数
于是可以把题目转成一个标准模型:
- 任选
为根 - 设
表示从根到 的路径点权和 - 对于两点
,它们路径和可以用 LCA 拆出来
设
- 根到
的路径和是 - 根到
的路径和是 - 根到
的那一段被重复算了两次
所以答案是:
最后为什么还要加回
因为:
被减了两次 - 但 LCA 点本身在真实路径里应该保留一次
因此只要预处理好:
- 每个点的度数
- 每个点到根的路径和
- 倍增 LCA
每次询问就能在
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100000 + 5;
const int LOG = 18;
int n, m;
vector<int> g[MAXN];
int deg[MAXN];
int depth_arr[MAXN];
int up[MAXN][LOG];
long long prefix_sum[MAXN]; // 根到当前点路径上的点权和(点权 = 度数)
void init_graph(int n) {
for (int i = 1; i <= n; i++) {
g[i].clear();
deg[i] = 0;
depth_arr[i] = 0;
prefix_sum[i] = 0;
for (int j = 0; j < LOG; j++) {
up[i][j] = 0;
}
}
}
void add_edge(int u, int v) {
g[u].push_back(v);
g[v].push_back(u);
deg[u]++;
deg[v]++;
}
void build_lca(int root) {
vector<int> st;
st.push_back(root);
up[root][0] = 0;
depth_arr[root] = 0;
prefix_sum[root] = deg[root];
while (!st.empty()) {
int u = st.back();
st.pop_back();
for (size_t i = 0; i < g[u].size(); i++) {
int v = g[u][i];
if (v == up[u][0]) {
continue;
}
up[v][0] = u;
depth_arr[v] = depth_arr[u] + 1;
prefix_sum[v] = prefix_sum[u] + deg[v];
for (int j = 1; j < LOG; j++) {
up[v][j] = up[up[v][j - 1]][j - 1];
}
st.push_back(v);
}
}
}
int kth_ancestor(int u, int k) {
for (int j = 0; j < LOG; j++) {
if (k & (1 << j)) {
u = up[u][j];
}
}
return u;
}
int lca(int a, int b) {
if (depth_arr[a] < depth_arr[b]) {
swap(a, b);
}
a = kth_ancestor(a, depth_arr[a] - depth_arr[b]);
if (a == b) {
return a;
}
for (int j = LOG - 1; j >= 0; j--) {
if (up[a][j] != up[b][j]) {
a = up[a][j];
b = up[b][j];
}
}
return up[a][0];
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
init_graph(n);
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
add_edge(u, v);
}
build_lca(1);
while (m--) {
int u, v;
cin >> u >> v;
int p = lca(u, v);
long long ans = prefix_sum[u] + prefix_sum[v] - 2LL * prefix_sum[p] + deg[p];
cout << ans << '\n';
}
return 0;
}复杂度
预处理:
- 建树和点度数统计:
- 倍增祖先表:
每次查询:
- 求一次 LCA:
空间复杂度:
总结
这题虽然题面说的是“网络传输时间”,但真正落到算法上只有一句话:
- 路径代价 = 路径上所有点的度数和
一旦看成树上路径点权和,就会自然想到:
- 根到点前缀和
- LCA 拆路径
所以它本质是一道很标准的:
倍增 LCA + 路径点权和
的树上查询题。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
