树上启发式合并(dsu on tree)
树上启发式合并用于离线处理一些树上问题,通常是子树询问。
树上启发式合并的本质是链剖分。
算法流程:
第一次 dfs,预处理每个点的父亲、子树大小、重儿子(通常是子树大小最大的儿子),类似树链剖分的第一次 dfs。
第二次 dfs,设当前 dfs 到点
代码:
const int N=1e5+3;
typedef int ar[N];
ar sn,sz,fa,l,r,p;//l为dfs序编号,r为子树dfs序的编号最大值,p为dfs序编号对应的点编号
int id;
basic_string<int>g[N];
void pre(int x,int y)//第一次dfs
fa[x]=y,sz[x]=1,p[l[x]=++id]=x;
for(int i:g[x])if(i!=y)if(pre(i,x),sz[x]+=sz[i],sz[i]>sz[sn[x]])sn[x]=i;
r[x]=id;
}
void add(int x){}//加入一个点
void del(int x){}//删除一个点
void dfs(int x,bool b){//b=1表示需要删除,否则不需要
for(int i:g[x])if(i!=fa[x]&&i!=sn[x])dfs(i,1);//dfs轻儿子
if(sn[x])dfs(sn[x],0);//dfs重儿子
add(x);//加入点x
for(int i:g[x])if(i!=fa[x]&&i!=sn[x])for(int j=l[i];j<=r[i];++j)add(p[j]);//加入轻子树
//处理点x的询问省略
if(b)for(int i=l[x];i<=r[x];++i)del(p[i]);//删除所有点
}
int main(){
//读入等部分省略
pre(1,0),dfs(1,0);
return 0;
}
时间复杂度:
空间复杂度:
时间复杂度证明:
每个点的某个轻子树大小一定小于这个点的子树大小的一半,所以根结点到任意点的路径上轻边(即父亲到轻儿子的边)不超过
dfs 到点
在部分题目(如 uoj #284)中,维护的信息和深度有关,可以用长链剖分代替重链剖分,将子树深度最深的儿子作为重儿子,复杂度
总结:
对于一类树上可离线的子树询问问题,若可
若可以
习题:
CF246E Blood Cousins Return
CF600E Lomsat gelral
CF375D Tree and Queries
P4149 [IOI2011]Race
CF741D Arpa’s letter-marked tree and Mehrdad’s Dokhtar-kosh paths
CF715C Digit Tree
uoj #284. 快乐游戏鸡(长链剖分)
P3714 [BJOI2017]树的难题(长链剖分)
参考资料:
OI Wiki 树上启发式合并
CF741D 作者 Arpa 的 blog