题目
Allen的树形地铺
说明
你知道吗,Allen在机房睡了一暑假的地铺!
Allen有一棵nn个节点的树,有qq个查询,每个查询给出两个节点u,vu,v,你需要输出在删除节点uu及其所连边的情况下,节点vv所在的连通块的大小。
每个询问之间是独立的。也就是说,每次询问不会真的删除节点uu。
请回忆:一棵nn个节点的树是一个含有nn个点,n−1n−1条边的无向连通图。
输入格式
第一行输入一个正整数n (1≤n≤3×105)n (1≤n≤3×105),代表给定树的节点数量。
接下来输入n−1n−1行,每行给出2个正整数u,v (1≤u,v≤n,u≠v)u,v (1≤u,v≤n,u=v),代表存在一条边连接节点u,vu,v。保证给定的图形成一棵树。
接下来输入一个正整数q (1≤q≤3×105)q (1≤q≤3×105),代表询问的个数。
接下来输入qq行,每行2个正整数u,v (1≤u,v≤n,u≠v)u,v (1≤u,v≤n,u=v),代表给定的询问。
输出格式
输出qq行。每行输出1个整数,代表在删除节点uu及其所连边的情况下,节点vv所在的连通块大小。
AC code
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
const int N = 300005 + 5; // 最大节点数 + 5(多分配一些空间避免越界)
int f[N][20]; // f[u][j] 表示节点u向上跳2^j步到达的祖先节点
int dep[N]; // dep[u] 表示节点u在树中的深度(根节点深度为1)
vector<int> vec[N]; // 邻接表,存储树的边关系
int siz[N]; // siz[u] 表示以u为根的子树的节点数(包含u自身)
// DFS函数:深度优先遍历树,预处理所有需要的信息
// u: 当前遍历的节点
// fa: u的父节点(根节点的父节点设为0)
void dfs(int u, int fa) {
// 设置u的第0级祖先(2^0 = 1步)就是父节点fa
f[u][0] = fa;
// 计算u的深度:父节点深度+1
dep[u] = dep[fa] + 1;
// 初始化以u为根的子树大小为1(只包含u自己)
siz[u] = 1;
// 预处理倍增表:计算u的所有2^j级祖先
// 原理:u的2^j级祖先 = u的2^(j-1)级祖先的2^(j-1)级祖先
for (int i = 1; i < 20; i++) {
f[u][i] = f[f[u][i - 1]][i - 1];
// 例如:u的爷爷(2^1级祖先)= u的爸爸的爸爸
// 例如:u的曾祖父(2^2级祖先)= u的爷爷的爷爷
}
// 遍历u的所有邻居节点(子节点)
for (int v : vec[u]) {
// 跳过父节点(避免无限递归)
if (v == fa) continue;
// 递归遍历子节点v,u是v的父节点
dfs(v, u);
// 累加子节点v的子树大小到u的子树大小中
siz[u] += siz[v];
}
}
// LCA函数:求两个节点a和b的最近公共祖先
int lca(int a, int b) {
// 确保a是深度较大的节点,方便后面的处理
if (dep[b] > dep[a]) swap(a, b);
// 第一步:让深度较大的a跳到与b同一深度
// tmp是需要跳的步数 = a的深度 - b的深度
int tmp = dep[a] - dep[b];
// 使用二进制分解法:把tmp拆分成2的幂次之和
// 例如:tmp=5(二进制101),表示需要跳2^0 + 2^2步
for (int j = 0; tmp; j++, tmp >>= 1) {
// 如果tmp的二进制第j位是1,就跳2^j步
if (tmp & 1) a = f[a][j];
}
// 如果此时a和b相同,说明b就是a的祖先,直接返回
if (a == b) return a;
// 第二步:a和b同时向上跳,直到它们的父亲相同
// 从大步长开始尝试(跳大步长不会错过LCA)
for (int j = 19; j >= 0; j--) {
// 如果跳2^j步后祖先不同,说明LCA还在更上面
// 就可以安全地跳这一步(如果跳2^j步后相同,可能是跳过头了)
if (f[a][j] != f[b][j]) {
a = f[a][j]; // a跳2^j步
b = f[b][j]; // b跳2^j步
}
}
// 此时a和b的父亲就是它们的LCA
return f[a][0];
}
// 查找祖先函数:求节点u向上跳k步后的祖先
int findzuxian(int u, int k) {
// 同样使用二进制分解法
for (int j = 0; k; j++, k >>= 1) {
// 如果k的二进制第j位是1,就跳2^j步
if (k & 1) u = f[u][j];
}
return u;
}
signed main() {
// 优化输入输出,加快速度
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n, q;
cin >> n; // 读入节点数
// 读入n-1条边,构建树
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
// 无向图,两边都要添加
vec[u].push_back(v);
vec[v].push_back(u);
}
// 从节点1开始DFS,0作为根节点的父节点
dfs(1, 0);
cin >> q; // 读入查询数量
// 处理每个查询
for (int i = 0; i < q; i++) {
int u, v;
cin >> u >> v;
// 求u和v的最近公共祖先
int fa = lca(u, v);
if (fa == u) {
// 情况1:u是v的祖先
// 删除u后,v所在的连通块是以"u到v路径上u的下一个节点"为根的子树
// 这个节点是v向上跳(dep[v] - dep[u] - 1)步到达的
// 例如:u是v的爷爷,需要找到v的爸爸
int son = findzuxian(v, dep[v] - dep[u] - 1);
cout << siz[son] << endl; // 输出这个子树的节点数
} else {
// 情况2:u不是v的祖先
// 删除u后,v所在的连通块是整棵树去掉以u为根的子树
cout << n - siz[u] << endl;
}
}
return 0;
}
看不懂思密达