Dsu on tree
简介
Dsu on tree(树上启发式合并)是一种统计树上一个节点的子树中具有某种特征的节点数的算法 如CF 600E 。本质上是利用轻重链剖分对爆搜优化。
如果一个问题具有如下性质:
1.只有对子树的查询
2.没有修改(静态)
基本上就可以直接怼Dsu on tree了。
算法讲解
看问题实例:
给出一棵树,每个节点有一种颜色。
求每个节点以当前节点为根的子树中出现次数最多的颜色的编号和。
(其实就是CF600E)
首先想想如何爆搜:
遍历每个节点,每个节点统计一遍颜色有多少个。完事之后再消除当前节点的贡献,继续递归。
每个节点都要遍历一遍子树,时间复杂度为(O(n^2))
显然不够优秀。
那么我们来看看它都做了些什么无用功
对于最后一次搜索,它的结果是不用清空的,因为它的答案可以用于父节点答案统计。
那我们可以试着留住尽量大的子树,也就是重儿子,那么就树剖一遍求得重儿子,回溯时不擦除就行了。
关于时间复杂度,因为每个点的轻边只有 (O(log n)) 条,故时间复杂度为 (O(nlog n))。
这里Orz一下发明人,把暴力玩到这么优雅( (
核心代码模板:
void dfs(int x,int f,int p)
/*x:当前节点 f:当前节点父节点 p:当前节点是否需要保留*/
{
for(/*遍历所有相邻节点*/)
{
int y=/*遍历到的节点*/
if(/*y不是重儿子或父节点*/) dfs(y,x,0);
}
if(/*当前节点有重儿子*/) dfs(/*重儿子*/,x,1),Son=/*重儿子编号*/; //统计重儿子,不消除影响
/*统计所有轻儿子的答案*/
/*更新答案并删除贡献*/
}
CF600E code:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+10;
int head[N],ver[N<<1],nxt[N<<1],tot=0;
void add(int x,int y)
{
ver[++tot]=y;
nxt[tot]=head[x];
head[x]=tot;
}
int n;
int col[N];
int son[N],size_[N],cnt[N];
int maxn,Son;
ll sum=0,ans[N];
void dfs1(int x,int f)//轻重链剖分
{
size_[x]=1;
for(int i=head[x];i;i=nxt[i])
{
int y=ver[i];
if(y==f) continue;
dfs1(y,x);
size_[x]+=size_[y];
if(size_[son[x]]<size_[y]) son[x]=y;
}
}
void add_(int x,int f,int val)//统计答案
/*val=1 统计答案 val=-1 删去答案*/
{
cnt[col[x]]+=val;
if(cnt[col[x]]>maxn) maxn=cnt[col[x]],sum=col[x];
else if(cnt[col[x]]==maxn) sum+=col[x];
for(int i=head[x];i;i=nxt[i])
{
int y=ver[i];
if(y==f||y==Son) continue;//重儿子不管
add_(y,x,val);
}
}
void dfs2(int x,int f,int p)/*p=0 需要消除影响*/
{
for(int i=head[x];i;i=nxt[i])
{
int y=ver[i];
if(y==f) continue;
if(y!=son[x]) dfs2(y,x,0);
}
if(son[x]) dfs2(son[x],x,1),Son=son[x];//统计重儿子,不消除影响
add_(x,f,1),Son=0;//统计所有轻儿子的贡献
ans[x]=sum; //更新答案
if(!p) add_(x,f,-1),sum=0,maxn=0;//删除贡献
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",col+i);
for(int i=1;i<n;i++)
{
int x,y;
scanf("%d%d",&x,&y);
add(x,y);
add(y,x);
}
dfs1(1,1);
dfs2(1,1,0);
for(int i=1;i<=n;i++)
{
printf("%lld ",ans[i]);
}
return 0;
}