dsu on tree,又名树上启发式合并、重链剖分,是一类十分实用的trick,它常常可以作为一些正解的替代算法:
1.DFS序+线段树/主席树/线段树合并
2.对DFS序分块的树上莫队
3.长链剖分(但复杂度会多一个log)
4.点分治(通常可以做有根树的点分治)
重链剖分的概念,用一个DFS找到每个点最大的一个儿子,作为它的重儿子,并将它标记。则从上到下一段连续的标记点就成为一条重链。
重链剖分有一个常用的性质:每个点到根的路径上,至多经过$O(\log n)$条重链。点分治、树链剖分都用到了这个性质。
dsu on tree 是一个优化后的暴力,主要优化的地方在于它先递归轻子树并消除影响,后递归重子树并保留影响。之后再计算该节点需要的信息。
它可以解决大部分无修改子树查询问题,需要问题满足以下几个条件:
1.有一个树上的$O(n^2)$暴力算法。
2.从轻子树合并上来的复杂度是线性的。
3.从重子树合并上来的复杂度是$O(1)$的。
4.可以在O(子树大小)时间内清空递归后的数组(也就是线性撤销所有影响)
dsu on tree的流程:
1.递归到所有轻儿子并消除影响。
2.递归到重儿子并保留影响。
3.递归所有轻儿子计算子树内除重子树之外的点对当前点答案的影响。
4.若此点不是父亲的重儿子则消除子树内所有影响(即将数组清空)。
另外,dsu on tree的题,只要使用的暴力数组是以深度为下标的,几乎都可以被长链剖分替代且复杂度少一个log。如下面的例2,3。
例一:[CF600E]询问每棵子树中出现次数最多的颜色(可能不只一个)的编号和。
首先考虑暴力算法,对每个点x DFS下去,得到一个计数器数组co[i]表示x的子树内颜色i的点的个数,同时维护x的答案。
然后重链剖分,按流程做即可,具体模板见代码。
1 #include<cstdio> 2 #include<algorithm> 3 #include<iostream> 4 #define rep(i,l,r) for (int i=(l); i<=(r); i++) 5 #define For(i,x) for (int i=h[x],k; i; i=nxt[i]) 6 typedef long long ll; 7 using namespace std; 8 9 const int N=1000010; 10 int n,u,v,cnt,tot[N],mx,col[N],sz[N],son[N],h[N],to[N],nxt[N]; 11 ll ans[N],sm; 12 bool skip[N]; 13 14 void add(int u,int v){ to[++cnt]=v; nxt[cnt]=h[u]; h[u]=cnt; } 15 16 void get(int x,int fa){ 17 sz[x]=1; 18 For(i,x) if ((k=to[i])!=fa){ 19 get(k,x); sz[x]+=sz[k]; 20 if (sz[k]>sz[son[x]]) son[x]=k; 21 } 22 } 23 24 void dfs(int x,int fa,int op){ 25 tot[col[x]]+=op; 26 if (op>0 && tot[col[x]]>=mx){ 27 if (tot[col[x]]>mx) sm=0,mx=tot[col[x]]; 28 sm+=col[x]; 29 } 30 For(i,x) if ((k=to[i])!=fa && !skip[k]) dfs(k,x,op); 31 } 32 33 void work(int x,int fa,bool cl){ 34 For(i,x) if ((k=to[i])!=fa && k!=son[x]) work(k,x,1); 35 if (son[x]) work(son[x],x,0),skip[son[x]]=1; 36 dfs(x,fa,1); ans[x]=sm; skip[son[x]]=0; 37 if (cl) dfs(x,fa,-1),mx=sm=0; 38 } 39 40 int main(){ 41 scanf("%d",&n); 42 rep(i,1,n) scanf("%d",&col[i]); 43 rep(i,2,n) scanf("%d%d",&u,&v),add(u,v),add(v,u); 44 get(1,0); work(1,0,0); 45 rep(i,1,n) cout<<ans[i]<<' '; 46 return 0; 47 }