爬树题解

爬树 题解

爬树(mako)

题目概述

给定一颗树,有两种转移方法,1.从子节点到父节点并消耗权值,2.从一个节点转移到同深度的另一个节点\((要求两个节点间的距离<=2*d)\)然后消耗c权值,求每个节点到根的最短距离

思考过程

首先我们要由特殊到一般

  1. 先考虑一条链的情况,直接统计一个点逐步向下转移到最后的答案就行
    然后考虑d=0的情况,也只需要逐步统计就行了
    以上为20分答案

  2. 考虑菊花图的情况

    \(n<=1e3\) 的情况下,菊花图只需要让同层两节点之间建立边并且边权为c,然后跑一遍最短路就行
    \(n<=2*1e5\) 的情况下,我们发现如果建立边,会导致\(n^2\)使空间内存都爆掉,所以考虑优化。

    通过对一个点的分析,一种就是由直接转移而来,还有一种就是通过同层转移而来,而我们贪心而想,如果都是同层转移,那么要做到使同层的点的权值是最小的,这样可以做到最小的答案,之后去做比较选择,而式子为以下:

    \[dis[v] = min(dis[u]+val[v],dis[MINU]+val[MINV]+c) \]

  3. 通过由特殊到一般,我们思考出了解决方法,接下来考虑实现

实现

  1. 实现的难点在于,我们需要去维护一个点,在同深度所对应的每个不超过d距离的最小值,如果直接暴力枚举,是至少为\(O(n^2)\)的,所以我们考虑怎么优化

  2. 通过考虑树的特殊性质,在同层的节点,进入顺序与离最开始的点的距离是成单调递增的,所以我们只需要去维护一个vector,使用双指针(滑动窗口(限制大小为d))去寻找一个点所有对应的点与其最小值,时间是\(O(n)\)

  3. 实现代码如下

    #include <bits/stdc++.h>
    #define N 1000006
    using namespace std;
    #define int long long
    const int inf = 1e18;
    
    int a[N], depp[N], fa[N][21],
        dis[N];  // a数组存储每个节点的e_x;dep数组存储节点深度;fa数组用于LCA的倍增表;dis数组存储每个节点到地面的最小体力值
    int n, d, C;
    vector<int> G[N], D[N];  // D[dep]存储所有深度为dep的节点
    
    // 深度优先搜索,用于初始化每个节点的深度,并将同深度的节点存入D数组
    void dfs(int u) {
        D[depp[u]].push_back(u);  // 将当前节点u按深度存入对应的D数组中
        for (int v : G[u]) {      // 遍历u的邻接节点
            if (v == fa[u][0])    
                continue;
            fa[v][0] = u;           // 记录v的直接父节点
            depp[v] = depp[u] + 1;  // 计算v的深度
            dfs(v);                 // 递归处理
        }
    }
    
    // 倍增法求最近公共祖先
    int lca(int x, int y) {
        if (x == y)  // 如果x和y是同一个节点,直接返回
            return x;
        for (int i = 20; ~i; --i) {      // 从大到小枚举倍增的步数
            if (fa[x][i] != fa[y][i]) {  // 如果x和y的2^i级祖先不同
                x = fa[x][i];            
                y = fa[y][i];            
            }
        }
        return fa[x][0];  // 最后x和y的直接父节点就是LCA
    }
    
    // 计算x到lca(x,y)的距离(即x在LCA到x路径上的边数)
    int find(int x, int y) { return depp[x] - depp[lca(x, y)]; }
    
    signed main() {
        freopen("mako.in", "r", stdin);
        freopen("mako.out", "w", stdout);
        ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    
        cin >> n >> d >> C;
        for (int i = 2; i <= n; ++i) cin >> a[i];  // 读取每个节点(除1号根节点)的e_x
        for (int x, y, i = 1; i < n; ++i) {
            cin >> x >> y;
            G[x].push_back(y), G[y].push_back(x);
        }
    
        fa[1][0] = 1;  // 根节点1的直接父节点是自己
        dfs(1);        // 从根节点1开始DFS,初始化深度和D数组
    
        // 预处理倍增表,用于快速查询祖先
        for (int i = 1; i < 21; ++i) {
            for (int j = 1; j <= n; ++j) fa[j][i] = fa[fa[j][i - 1]][i - 1];
        }
    
        dis[0] = inf;                   // 深度为0无意义,初始化为无穷大
        for (int i = 1; i <= n; ++i) {  // 按深度从小到大处理每个节点
            for (int j : D[i]) {        // 先计算从父节点下来的体力消耗(第一种移动方式)
                dis[j] = dis[fa[j][0]] + a[j];
            }
            // 处理同深度内的移动(第二种移动方式),用滑动窗口找同深度内距离不超过d的区间里的最小体力值
            for (int l = 0, r = -1; l < D[i].size(); l = r + 1) {
                // 扩展右边界,找到当前左端点l对应的最大右区间r,使得区间内节点与l的距离不超过d
                while (r + 1 < D[i].size() && find(D[i][l], D[i][r + 1]) <= d) ++r;
                int x = 0;
                // 在[l, r]区间内找到体力值最小的节点x
                for (int j = l; j <= r; ++j) {
                    if (dis[D[i][j]] < dis[x])
                        x = D[i][j];
                }
                // 用x的体力值 + C 更新区间内所有节点的最小体力值
                for (int j = l; j <= r; ++j) {
                    dis[D[i][j]] = min(dis[D[i][j]], dis[x] + C);
                }
            }
        }
    
        for (int i = 1; i <= n; ++i) cout << dis[i] << " ";
        return 0;
    }
    
posted @ 2025-11-03 19:01  Yuriha  阅读(1)  评论(0)    收藏  举报