「ZJOI2015」幻想乡战略游戏
结论,点分树

傲娇少女幽香与赤色杀人魔私密合影流出
简述
给定一棵 个节点的树,点有点权,边有边权,初始时各点点权为 。定义树上一点 作为决策点的代价为:
其中 表示树上两点距离, 表示点权。
定义作为决策点代价最小的点为带权重心。给定 次操作,每次将给定节点的点权增加给定值 ,并查询当前树上带权重心的代价。
所有节点的度数不超过 。
,, 边权 。
6S,256MB。
分析
先随意钦定一个点 为树根。若带权重心在 的儿子 的子树中,则显然 作为决策点的代价小于 作为决策点的代价。记 表示此时树上 的子树中各点的权值之和,则决策点从 移动到 代价的增量为:
由于 ,由上式可知当 时 作为决策点优于 作为决策点。且显然满足该式的 至多只有一个。点分治总结 - Sshwy's Notes 中给出了一个对该结论的证明。
注意到节点度数不超过 20,我们可以进行一些基于枚举儿子的算法。
根据上述性质可以得到一个基于换根的做法:先随机一个点作为根并求得其作为决策点的代价,之后枚举它的儿子 ,检查带权重心是否位于儿子 中,若位于儿子中则换根,更新 并得到 作为决策点的代价。依次进行直到不存在更优的儿子即可。暴力实现复杂度依赖于树高,是个极其不稳定的算法,且无法高效率处理修改操作。
上述带权重心问题,实质上是一类路径统计问题。为减小树高并处理修改,可以考虑将上述过程放到点分树上进行。
从根开始向下查询。对于每个分治块,先计算重心作为决策点的代价,再枚举重心的儿子 并计算它们分别作为决策点的代价。若儿子 作为决策点时优于重心,则钦定 所在分治块的重心作为新的决策点。不断递归进行,直至不存在更优的儿子。
考虑在上述过程中如何快速维护某点作为决策点的代价。
根据点分树上 的性质,考虑维护每个点作为决策点时点分树子树内各点的贡献,并通过暴力枚举 统计不在其子树内的节点与指定节点构成的路径的贡献。记 表示点分树中 的子树内各点的点权之和, 表示点分树中 的子树内各点对决策点 的贡献之和, 表示点分树中 的子树内各点对决策点 的父亲的贡献之和,即有:
求某点作为决策点的代价时,在点分树上模拟点分治的过程,先统计子树贡献 ,再暴力跳父亲统计不在子树内节点的贡献(即过父亲的路径的贡献),每次累计不在指定点所在分治块内的点到达父亲的路径的贡献,再加上指定点到达父亲的路径的贡献即可。单次查询复杂度 级别。
又点分树的高为 级别,且每个节点度数不超过 20。使用 RMQ 求得两点间距离的前提下,总复杂度为 再乘上一个 20 的大常数(
但是一般跑不满,实际表现比较出色。
代码
复制复制//知识点:点分树 /* By:Luckyblock */ #include <algorithm> #include <cctype> #include <cstdio> #include <cstring> #include <vector> #define LL long long const int kN = 1e5 + 10; const int kM = kN << 1; //============================================================= int n, m, e_num, head[kN], v[kM], w[kM], ne[kM], newroot[kM]; int allroot, root, sumsz, sz[kN], maxsz[kN], newfa[kN]; LL sum[kN], sumval[kN], sumfaval[kN]; //sum[u]:u 点分树子树内各点点权之和 //sumval[u]:u 点分树子树内各点到达 u 的代价 dis(u,v)*val[v] 之和 //sumfaval[u]:u 点分树子树内各点到达 u 点分树上的父亲 fa[u] 的代价之和 bool vis[kN]; //============================================================= inline int read() { int f = 1, w = 0; char ch = getchar(); for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1; for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0'); return f * w; } void Chkmax(int &fir, int sec) { if (sec > fir) fir = sec; } void Chkmin(int &fir, int sec) { if (sec < fir) fir = sec; } void Add(int u_, int v_, int w_) { v[++ e_num] = v_, w[e_num] = w_; ne[e_num] = head[u_], head[u_] = e_num; } namespace ST { //用于求树上两点距离 int num, Log2[kN << 1], f[kN << 1][22], fir[kN], dep[kN]; LL dis[kN]; void Dfs(int u_, int fa_) { dep[u_] = dep[fa_] + 1; fir[u_] = ++ num; f[num][0] = u_; for (int i = head[u_]; i; i = ne[i]) { int v_ = v[i], w_ = w[i]; if (v_ == fa_) continue ; dis[v_] = dis[u_] + w_; Dfs(v_, u_); f[++ num][0] = u_; } } void Prepare() { Dfs(1, 0); Log2[1] = 0; for (int i = 2; i <= num; ++ i) Log2[i] = Log2[i >> 1] + 1; for (int i = 1; i <= 21; ++ i) { for (int j = 1; j + (1 << i) - 1 <= num; ++ j) { if (dep[f[j][i - 1]] < dep[f[j + (1 << (i - 1))][i - 1]]) { f[j][i] = f[j][i - 1]; } else { f[j][i] = f[j + (1 << (i - 1))][i - 1]; } } } } int Lca(int u_, int v_) { int l = fir[u_], r = fir[v_]; if (l > r) std::swap(l, r); int lth = Log2[r - l + 1]; if (dep[f[l][lth]] < dep[f[r - (1 << lth) + 1][lth]]) return f[l][lth]; return f[r - (1 << lth) + 1][lth]; } int Dis(int u_, int v_) { return dis[u_] + dis[v_] - 2 * dis[Lca(u_, v_)]; } } void CalcSize(int u_, int fa_) { sz[u_] = 1, maxsz[u_] = 0; for (int i = head[u_]; i; i = ne[i]) { int v_ = v[i]; if (v_ == fa_ || vis[v_]) continue; CalcSize(v_, u_); Chkmax(maxsz[u_], sz[v_]); sz[u_] += sz[v_]; } Chkmax(maxsz[u_], sumsz - sz[u_]); if (maxsz[u_] < maxsz[root]) root = u_; } void Dfs(int u_, int fa_) { vis[u_] = true; for (int i = head[u_]; i; i = ne[i]) { int v_ = v[i]; if (v_ == fa_ || vis[v_]) continue; sumsz = sz[v_], root = 0, maxsz[root] = kN; CalcSize(v_, u_); newroot[i] = root; //处理 v 所在分治块的重心 newfa[root] = u_; CalcSize(root, 0), Dfs(root, 0); } } void Modify(int pos_, int val_) { //单点修改操作 sum[pos_] += val_; //按照定义跳父亲更新 for (int u_ = pos_; newfa[u_]; u_ = newfa[u_]) { int f = newfa[u_], dis = ST::Dis(pos_, newfa[u_]); sumval[f] += 1ll * dis * val_; sumfaval[u_] += 1ll * dis * val_; sum[f] += val_; } } LL Calc(int pos_) { //模拟点分治过程,计算以 pos_ 为带权重心时的代价之和 LL ret = sumval[pos_]; //以 pos_ 为重心的分治块的贡献 for (int u_ = pos_; newfa[u_]; u_ = newfa[u_]) { int f = newfa[u_]; //统计当前分治块内过 f 的路径的贡献 ret += sumval[f] - sumfaval[u_] + //所有节点到 f 的代价,并去除不合法的在下一层的节点的代价 ST::Dis(f, pos_) * (sum[f] - sum[u_]); //从 f 到 pos_ 的路径代价 } return ret; } LL Query(int u_) { //自根向下不断寻找更优解 LL ret = Calc(u_); for (int i = head[u_]; i; i = ne[i]) { int v_ = v[i], newroot_ = newroot[i]; if (Calc(v_) < ret) return Query(newroot_); } return ret; } void Init() { n = read(), m = read(); for (int i = 1; i < n; ++ i) { int u_ = read(), v_ = read(), w_ = read(); Add(u_, v_, w_), Add(v_, u_, w_); } ST::Prepare(); sumsz = n, root = 0; maxsz[root] = kN; CalcSize(1, 0), CalcSize(root, 0); allroot = root; Dfs(root, 0); } //============================================================= int main() { Init(); for (int i = 1; i <= m; ++ i) { int pos_ = read(), val_ = read(); Modify(pos_, val_); printf("%lld\n", Query(allroot)); } return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】