一、概述
(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)))的