P2664 树上游戏

知识点:点分治

原题面 Luogu

简述

给定一棵 n 个节点的树,节点 i 的颜色为 ci。定义 s(i,j) 表示节点 i 到节点 j 简单路径上的颜色种类数,定义 sumi=1jns(i,j),求:sum1sumn
1n,ci105
1S,128MB。

分析

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

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

再考虑对非根节点的贡献。考虑在上一步中预处理一些值:所有链的贡献量 (即sumroot 的增量) S,各颜色有贡献的路径数量 f。若在上一步中 dfs 到节点 u 时有 cntcu=1,则令 SS+sizeufcufcu+sizeu

之后考虑 dfs 处理非根节点的答案。对于 dfs 到的节点 u,设 u 所在的 root 的儿子为 son,过根节点且过节点 u 的路径显然有 sizerootsizeson 条。将这样的路径 uv 拆成链 rootu 与链 rootv 分别考虑它们对 sumu 的贡献。

对于链 rootu,它在每一条有贡献的路径中都被包含。考虑在 dfs 枚举 u 时通过维护当前节点到根路径上各颜色出现的次数,处理出其中的颜色种类数,乘上路径数量 sizerootsizeson 即为其贡献。

对于所有链 rootv,先考虑从所有链的贡献 S 与各颜色的贡献 f 中减去子树 son 的贡献。具体地,考虑枚举子树 son 的节点 x。若某颜色在 rootx 的路径上是第一次出现,则令 SSsizeufcufcusizeu。处理完成后 Sf 仅包含其他子树的信息。之后进行 dfs 枚举节点 u 并更新 sumu,若某颜色在路径 rootu 上出现,说明该颜色的贡献已经在上一步统计 rootu 时已经统计过,的则令 SSfcu。此时所有链 rootvsumu 的贡献即为 S。统计两种链的贡献可以在一个 dfs 中完成。

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

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

代码

复制复制
//知识点:点分治
/*
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 @   Luckyblock  阅读(136)  评论(0编辑  收藏  举报
编辑推荐:
· 如何编写易于单元测试的代码
· 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】
点击右上角即可分享
微信分享提示