其他分享
首页 > 其他分享> > 「ZJOI2015」幻想乡战略游戏

「ZJOI2015」幻想乡战略游戏

作者:互联网

原题面:LojLuogu

结论,点分树

傲娇少女幽香与赤色杀人魔私密合影流出

简述

给定一棵 \(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\) 的父亲的贡献之和,即有:

\[\begin{aligned} s_u &= \sum_{v\in \operatorname{subtree}(u)} \operatorname{val}_v\\ f_u &= \sum_{v\in \operatorname{subtree}(u)} \operatorname{val}_v\times \operatorname{dis}(u, v)\\ g_u &= \sum_{v\in \operatorname{subtree}(u)} \operatorname{val}_v\times \operatorname{dis}(\operatorname{fa}_u, v) \end{aligned}\]

求某点作为决策点的代价时,在点分树上模拟点分治的过程,先统计子树贡献 \(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