P2664 树上游戏

知识点:点分治

原题面 Luogu

简述

给定一棵 \(n\) 个节点的树,节点 \(i\) 的颜色为 \(c_i\)。定义 \(s(i,j)\) 表示节点 \(i\) 到节点 \(j\) 简单路径上的颜色种类数,定义 \(sum_i = \sum\limits_{1\le j\le n} s(i,j)\),求:\(sum_1\sim sum_n\)
\(1\le n,c_i\le 10^5\)
1S,128MB。

分析

套路地考虑点分治,先钦定一个根节点 \(root\),仅考虑处理过根节点的路径,并递归处理不过根节点的路径。

考虑先处理出以根为端点的链的颜色种类数,并更新 \(sum_{root}\)。从路径的角度统计贡献并不容易,考虑从颜色的角度统计。从 \(root\) 开始 dfs,并维护一个 \(\operatorname{cnt}\) 数组储存当前节点到根路径上各颜色出现的次数。若 dfs 到节点 \(u\) 时有 \(\operatorname{cnt}_{c_u} = 1\),说明以 \(u\) 的子树节点为一端点,根为一端点的路径上都会出现颜色 \(c_u\)\(sum_{root}\) 应增加 \(\operatorname{size}_{u}\)

再考虑对非根节点的贡献。考虑在上一步中预处理一些值:所有链的贡献量 (即\(sum_{root}\) 的增量) \(S\),各颜色有贡献的路径数量 \(f\)。若在上一步中 dfs 到节点 \(u\) 时有 \(\operatorname{cnt}_{c_u} = 1\),则令 \(S\gets S+\operatorname{size}_u\)\(f_{c_u}\gets {f}_{c_u} + \operatorname{size}_u\)

之后考虑 dfs 处理非根节点的答案。对于 dfs 到的节点 \(u\),设 \(u\) 所在的 \(root\) 的儿子为 \(son\),过根节点且过节点 \(u\) 的路径显然有 \(\operatorname{size}_{root} - \operatorname{size}_{son}\) 条。将这样的路径 \(u\rightarrow v\) 拆成链 \(root\rightarrow u\) 与链 \(root \rightarrow v\) 分别考虑它们对 \(sum_u\) 的贡献。

对于链 \(root \rightarrow u\),它在每一条有贡献的路径中都被包含。考虑在 dfs 枚举 \(u\) 时通过维护当前节点到根路径上各颜色出现的次数,处理出其中的颜色种类数,乘上路径数量 \(\operatorname{size}_{root} - \operatorname{size}_{son}\) 即为其贡献。

对于所有链 \(root \rightarrow v\),先考虑从所有链的贡献 \(S\) 与各颜色的贡献 \(f\) 中减去子树 \(son\) 的贡献。具体地,考虑枚举子树 \(son\) 的节点 \(x\)。若某颜色在 \(root\rightarrow x\) 的路径上是第一次出现,则令 \(S\gets S-\operatorname{size}_u\)\(f_{c_u}\gets {f}_{c_u} - \operatorname{size}_u\)。处理完成后 \(S\)\(f\) 仅包含其他子树的信息。之后进行 dfs 枚举节点 \(u\) 并更新 \(sum_u\),若某颜色在路径 \(root \rightarrow u\) 上出现,说明该颜色的贡献已经在上一步统计 \(root \rightarrow u\) 时已经统计过,的则令 \(S \gets S - f_{c_u}\)。此时所有链 \(root \rightarrow v\)\(sum_u\) 的贡献即为 \(S\)。统计两种链的贡献可以在一个 dfs 中完成。

注意统计完成后需要还原子树 \(son\) 的贡献。

上述过程均可在 \(O(n)\) 的时间复杂度内完成,总时间复杂度是常数巨大的 \(O(n\log n)\)

代码

//知识点:点分治
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kN = 1e5 + 10;
//=============================================================
int n, m, e_num, col[kN], head[kN], v[kN << 1], ne[kN << 1];
int root, sumsz, sz[kN], maxsz[kN];
LL deltasz, num, sum, cnt[kN], colsz[kN], ans[kN];
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_) {
  v[++ e_num] = v_, ne[e_num] = head[u_], head[u_] = e_num;
}
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 Dfs1(int u_, int fa_) { //求得所有链的贡献
  sz[u_] = 1;
  cnt[col[u_]] ++;
  for (int i = head[u_]; i; i = ne[i]) {
    int v_ = v[i];
    if (v_ == fa_ || vis[v_]) continue;
    Dfs1(v_, u_);
    sz[u_] += sz[v_];
  }
  if (cnt[col[u_]] == 1) {
    sum += sz[u_];
    colsz[col[u_]] += sz[u_];
  }
  -- cnt[col[u_]];
}
void Modify(int u_, int fa_, int val_) { //更改子树的贡献
  cnt[col[u_]] ++;
  if (cnt[col[u_]] == 1) { //
    sum += val_ * sz[u_];
    colsz[col[u_]] += val_ * sz[u_];
  }
  for (int i = head[u_]; i; i = ne[i]) {
    int v_ = v[i];
    if (v_ == fa_ || vis[v_]) continue;
    Modify(v_, u_, val_);
  }
  cnt[col[u_]] --;
}
void Dfs2(int u_, int fa_) { //统计链 root -> u 与 链 root -> v 的贡献 
  cnt[col[u_]] ++;
  if (cnt[col[u_]] == 1) { //颜色 col[u] 在链 root -> u 中出现过,需要去除该颜色在其他链 root -> v 中的贡献
    sum -= colsz[col[u_]]; 
    ++ num; //num 为链 root -> u 上的颜色种类数
  }
  ans[u_] += sum + num * deltasz; //更新 
  for (int i = head[u_]; i; i = ne[i]) {
    int v_ = v[i];
    if (v_ == fa_ || vis[v_]) continue;
    Dfs2(v_, u_);
  }
  if (cnt[col[u_]] == 1) { //回溯时还原 
    sum += colsz[col[u_]];
    -- num;
  }
  -- cnt[col[u_]];
}
void Clear(int u_, int fa_) {
  cnt[col[u_]] = colsz[col[u_]] = 0;
  for (int i = head[u_]; i; i = ne[i]) {
    int v_ = v[i];
    if (v_ == fa_ || vis[v_]) continue;
    Clear(v_, u_);
  }
}
void Dfs(int u_, int fa_) {
  vis[u_] = true;
  Dfs1(u_, fa_);
  ans[u_] += sum; //对 root 的贡献 
  for (int i = head[u_]; i; i = ne[i]) {
    int v_ = v[i];
    if (v_ == fa_ || vis[v_]) continue;
    cnt[col[u_]] ++;  //清除子树 v 的贡献。根 u 为必经点,需要初始化其数量 
    sum -= sz[v_], colsz[col[u_]] -= sz[v_];
    Modify(v_, u_, -1);
    cnt[col[u_]] --;

    deltasz = sz[u_] - sz[v_]; //路径总数 
    Dfs2(v_, u_);

    cnt[col[u_]] ++; //还原子树 v 的贡献 
    sum += sz[v_], colsz[col[u_]] += sz[v_];
    Modify(v_, u_, 1);
    cnt[col[u_]] --;
  }
  sum = num = 0; //注意清空 
  Clear(u_, fa_);

  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_), CalcSize(root, 0), Dfs(root, 0);
  }
}
//=============================================================
int main() { 
  n = read();
  for (int i = 1; i <= n; ++ i) col[i] = read();
  for (int i = 1; i < n; ++ i) {
    int u_ = read(), v_ = read();
    Add(u_, v_), Add(v_, u_);
  }
  
  sumsz = n;
  root = 0, maxsz[root] = kN;
  CalcSize(1, 0), CalcSize(root, 0), Dfs(root, 0);
  for (int i = 1; i <= n; ++ i) printf("%lld\n", ans[i]);
  return 0;
}
posted @ 2021-02-17 14:05  Luckyblock  阅读(125)  评论(0编辑  收藏  举报