2025武汉联合新生赛E solution

16 阅读5分钟

题目

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;
}

看不懂思密达