爬树题解
爬树 题解
题目概述
给定一颗树,有两种转移方法,1.从子节点到父节点并消耗权值,2.从一个节点转移到同深度的另一个节点\((要求两个节点间的距离<=2*d)\)然后消耗c权值,求每个节点到根的最短距离
思考过程
首先我们要由特殊到一般
-
先考虑一条链的情况,直接统计一个点逐步向下转移到最后的答案就行
然后考虑d=0的情况,也只需要逐步统计就行了
以上为20分答案 -
考虑菊花图的情况
在 \(n<=1e3\) 的情况下,菊花图只需要让同层两节点之间建立边并且边权为c,然后跑一遍最短路就行
在 \(n<=2*1e5\) 的情况下,我们发现如果建立边,会导致\(n^2\)使空间内存都爆掉,所以考虑优化。通过对一个点的分析,一种就是由直接转移而来,还有一种就是通过同层转移而来,而我们贪心而想,如果都是同层转移,那么要做到使同层的点的权值是最小的,这样可以做到最小的答案,之后去做比较选择,而式子为以下:
\[dis[v] = min(dis[u]+val[v],dis[MINU]+val[MINV]+c) \] -
通过由特殊到一般,我们思考出了解决方法,接下来考虑实现
实现
-
实现的难点在于,我们需要去维护一个点,在同深度所对应的每个不超过d距离的最小值,如果直接暴力枚举,是至少为\(O(n^2)\)的,所以我们考虑怎么优化
-
通过考虑树的特殊性质,在同层的节点,进入顺序与离最开始的点的距离是成单调递增的,所以我们只需要去维护一个vector,使用双指针(滑动窗口(限制大小为d))去寻找一个点所有对应的点与其最小值,时间是\(O(n)\)
-
实现代码如下
#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; }

浙公网安备 33010602011771号