「ZJOI2015」幻想乡战略游戏
作者:互联网
结论,点分树
傲娇少女幽香与赤色杀人魔私密合影流出
简述
给定一棵 \(n\) 个节点的树,点有点权,边有边权,初始时各点点权为 \(0\)。定义树上一点 \(u\) 作为决策点的代价为:
\[\sum_{v\in T}\operatorname{dis}(u,v)\times\operatorname{val}_v \]其中 \(\operatorname{dis}(u,v)\) 表示树上两点距离,\(\operatorname{val}\) 表示点权。
定义作为决策点代价最小的点为带权重心。给定 \(m\) 次操作,每次将给定节点的点权增加给定值 \(w\),并查询当前树上带权重心的代价。
所有节点的度数不超过 \(20\)。
\(1\le n,m\le 10^5\),\(|w|\le 10^3\),\(1\le\) 边权 \(\le 10^3\)。
6S,256MB。
分析
先随意钦定一个点 \(u\) 为树根。若带权重心在 \(u\) 的儿子 \(v\) 的子树中,则显然 \(v\) 作为决策点的代价小于 \(u\) 作为决策点的代价。记 \(\operatorname{s}_x\) 表示此时树上 \(x\) 的子树中各点的权值之和,则决策点从 \(u\) 移动到 \(v\) 代价的增量为:
\[\operatorname{dis}(u,v)\times ((\operatorname{s}_u - \operatorname{s}_v) - \operatorname{s}_v) \]由于 \(\operatorname{dis}(u,v)>0\),由上式可知当 \(\operatorname{s}_v>2\times \operatorname{s_u}\) 时 \(v\) 作为决策点优于 \(u\) 作为决策点。且显然满足该式的 \(v\) 至多只有一个。
注意到节点度数不超过 20,我们可以进行一些基于枚举儿子的算法。
根据上述性质可以得到一个基于换根的做法:先随机一个点作为根并求得其作为决策点的代价,之后枚举它的儿子 \(v\),检查带权重心是否位于儿子 \(v\) 中,若位于儿子中则换根,更新 \(\operatorname{s}\) 并得到 \(v\) 作为决策点的代价。依次进行直到不存在更优的儿子即可。暴力实现复杂度依赖于树高,是个极其不稳定的算法,且无法高效率处理修改操作。
上述带权重心问题,实质上是一类路径统计问题。为减小树高并处理修改,可以考虑将上述过程放到点分树上进行。
从根开始向下查询。对于每个分治块,先计算重心作为决策点的代价,再枚举重心的儿子 \(v\) 并计算它们分别作为决策点的代价。若儿子 \(v\) 作为决策点时优于重心,则钦定 \(v\) 所在分治块的重心作为新的决策点。不断递归进行,直至不存在更优的儿子。
考虑在上述过程中如何快速维护某点作为决策点的代价。
根据点分树上 \(\operatorname{dis}(u,v) = \operatorname{dis}(u,\operatorname{lca}(u,v)) + \operatorname{dis}(v, \operatorname{lca}(u,v))\) 的性质,考虑维护每个点作为决策点时点分树子树内各点的贡献,并通过暴力枚举 \(\operatorname{lca}\) 统计不在其子树内的节点与指定节点构成的路径的贡献。记 \(\operatorname{s}_u\) 表示点分树中 \(u\) 的子树内各点的点权之和,\(f_u\) 表示点分树中 \(u\) 的子树内各点对决策点 \(u\) 的贡献之和,\(g_u\) 表示点分树中 \(u\) 的子树内各点对决策点 \(u\) 的父亲的贡献之和,即有:
求某点作为决策点的代价时,在点分树上模拟点分治的过程,先统计子树贡献 \(f_u\),再暴力跳父亲统计不在子树内节点的贡献(即过父亲的路径的贡献),每次累计不在指定点所在分治块内的点到达父亲的路径的贡献,再加上指定点到达父亲的路径的贡献即可。单次查询复杂度 \(O(\log n)\) 级别。
又点分树的高为 \(\log n\) 级别,且每个节点度数不超过 20。使用 RMQ \(O(1)\) 求得两点间距离的前提下,总复杂度为 \(O(m\log^2 n)\) 再乘上一个 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;
}
标签:幻想,游戏,int,决策,节点,ZJOI2015,代价,operatorname,dis 来源: https://www.cnblogs.com/luckyblock/p/14417728.html