dsu on tree 学习笔记
一、概述
\(dsu\ on\ tree\)通常用于解决子树上的问题,要求无修改操作且允许离线。
对于这样的问题,我们以前学过了像树上莫队、点分治等做法,但\(dsu\ on\ tree\)的复杂度远优于他们
虽然\(dsu\ on\ tree\)复杂度优秀,但其实,它就是一个优雅的暴力:
遇到子树问题,最暴力的做法毫无疑问就是暴力枚举子树上的所有点统计答案,实际上\(dsu\ on\ tree\)就是这样做的。
只不过,\(dsu\ on \ tree\)有着优雅的思想:轻重链剖分,它借用对每个点轻儿子与重儿子贡献的分别处理,达到了\(\mathcal O(nlog(n))\)的复杂度。
二、实现
- 将询问离线,记录在子树的根节点上
- 遍历整棵树,对于节点\(u\),先计算它轻儿子的答案,计算后删除信息
- 计算它重儿子的答案,不删除信息
- 将重子树的信息合并到\(u\)上
- 暴力遍历\(u\)的轻子树,将轻子树的信息合并到\(u\)上
- 处理\(u\)处的询问
- 根据\(u\)是否是重儿子选择是否删除\(u\)的信息
这就是\(dsu\ on\ tree\)的思想了,大家可能不太理解,我们从一道例题来感受一下:
三、例题
CF600E
题意:
-
给定一棵\(n\)个节点的以\(1\)为根的树,每个节点都有一个颜色。
-
如果一种颜色在以\(x\)为根的子树内出现次数最多,称其在以\(x\)为根的子树中占主导地位。显然,同一子树中可能有多种颜色占主导地位。
-
你的任务是对于每一个\(i \in[1,n]\),求出以\(i\) 为根的子树中,占主导地位的颜色的编号和。
题解:
这是一道经典的\(dsu\ on\ tree\)题目了
考虑如何暴力做:显然可以维护\(w[i]\)表示\(i\)这种颜色出现的次数,同时记录\(ret\)表示目前处理的节点中出现次数最多的颜色的编号和。
当遍历到\(u\)时,首先我们枚举它的轻儿子递归下去,遍历轻儿子后,要删除轻子树节点对于\(w\)的影响
接着遍历重子树,这次我们保留这些节点的贡献
那么全局变量中已经保存了重子树的信息了,轻子树的信息我们直接暴力枚举,修改\(w\)与\(ret\)
遍历完后,该节点的答案就是\(u\)的答案,最后,删除该节点的贡献,也是暴力枚举它的子树中的所有节点并删去。
代码如下:
int hson[N],siz[N],w[N],mx,son;
ll ret,ans[N];
inline void dfs(int u,int f){
siz[u]=1;
for(int i=first[u];i;i=e[i].nxt){
int v=e[i].v;
if(v==f) continue;
dfs(v,u);
siz[u]+=siz[v];if(siz[v]>siz[hson[u]]) hson[u]=v;
}//轻重链剖分模板
}
inline void work(int u,int f,int tp){
w[col[u]]+=tp;//tp=1表示要增加这个节点的贡献,-1则是减去该节点的贡献
if(w[col[u]]>mx) mx=w[col[u]],ret=col[u];
else if(w[col[u]]==mx) ret+=col[u];//更新ret
for(int i=first[u];i;i=e[i].nxt){
int v=e[i].v;
if(v==f||v==son) continue;//son保存的是重儿子,不要遍历到重儿子去
work(v,u,tp);
}
}
inline void dsu(int u,int f,int tp){//tp=1表示不删除信息,tp=0表示要删除
for(int i=first[u];i;i=e[i].nxt){
int v=e[i].v;
if(v==f||v==hson[u]) continue;
dsu(v,u,0); //处理轻儿子,要删除信息
}
if(hson[u]) dsu(hson[u],u,1),son=hson[u];//遍历重儿子,不删除信息
work(u,f,1);//暴力遍历轻子树
son=0;//接下来删除贡献是暴力遍历整个子树而不仅是轻子树了
ans[u]=ret;
if(!tp) work(u,f,-1),mx=0,ret=0;//直接删除所有节点的贡献
}
看起来十分暴力吧?它的复杂度其实确实是\(\mathcal O(nlog(n))\)的,这里给出了粗略的证明:
首先,根据轻重链剖分的性质,每一个点到根的路径上至多有\(\mathcal O(log(n))\)条轻边。
考虑一个点\(u\)在什么时候会被遍历到:
\(u\)被一个祖先节点\(x\)统计当且仅当\(u\)在\(x\)的轻子树上,也就是说\(x-u\)的这条链第一条边是轻边,那么唯一一条轻边对应唯一一个\(x\),所以至多被统计\(\mathcal O(log(n))\)次
\(u\)被一个祖先节点遍历以删除贡献当且仅当\(x\)是一个轻儿子,在\(u\)的祖先中,轻儿子的数量不超过\(\mathcal O(log(n))\)个,于是也只会被遍历\(\mathcal O(log(n))\)次
综上,因为每个点的信息修改是\(\mathcal O(1)\)的,所以总复杂度是\(\mathcal O(nlog(n))\)的