dsu on tree题表
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 }
例二:[CF570D]一棵树,询问某棵子树指定深度的点能否构成回文。
用二进制记录每个深度的每个字母出现次数的奇偶性即可。
1 #include<cstdio> 2 #include<vector> 3 #include<algorithm> 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 using namespace std; 7 8 const int N=1000010; 9 char s[N]; 10 int n,m,x,y,cnt,skip,a[N],d[N],ans[N],fa[N],sz[N],son[N],to[N],nxt[N],h[N]; 11 struct P{ int x,y; }; 12 vector<P>ve[N]; 13 14 void add(int u,int v){ to[++cnt]=v; nxt[cnt]=h[u]; h[u]=cnt; } 15 16 void dfs(int x){ 17 d[x]=d[fa[x]]+1; sz[x]=1; 18 For(i,x){ 19 dfs(k=to[i]); sz[x]+=sz[k]; 20 if (sz[k]>sz[son[x]]) son[x]=k; 21 } 22 } 23 24 void get(int x){ 25 a[d[x]]^=1<<(s[x]-'a'); 26 For(i,x) if ((k=to[i])!=skip) get(k); 27 } 28 29 void work(int x,int op){ 30 For(i,x) if ((k=to[i])!=son[x]) work(k,1); 31 if (son[x]) work(son[x],0),skip=son[x]; 32 get(x); skip=0; int ed=ve[x].size()-1; 33 rep(i,0,ed) ans[ve[x][i].x]=__builtin_popcount(a[ve[x][i].y])<=1; 34 if (op) get(x); 35 } 36 37 int main(){ 38 freopen("570D.in","r",stdin); 39 freopen("570D.out","w",stdout); 40 scanf("%d%d",&n,&m); 41 rep(i,2,n) scanf("%d",&fa[i]),add(fa[i],i); 42 rep(i,1,n) scanf(" %c",&s[i]); 43 rep(i,1,m) scanf("%d%d",&x,&y),ve[x].push_back((P){i,y}); 44 dfs(1); work(1,1); 45 rep(i,1,m) if (ans[i]) puts("Yes"); else puts("No"); 46 return 0; 47 }
例三:
[CF246E]一个森林,求k级后代中多少种不同的权值。
[CF208E]给出一个森林,求和一个点有相同k级祖先的点有多少。
第二个问题倍增找到k级祖先后就是弱化版的第一个问题了。
与例二类似,对每个深度维护一个set记录出现过的权值数目。
注意由于流程的最后一步是清空整个数组,所以删除的时候不需要考虑记录次数的问题。
1 #include<set> 2 #include<map> 3 #include<string> 4 #include<cstdio> 5 #include<vector> 6 #include<algorithm> 7 #include<iostream> 8 #define rep(i,l,r) for (int i=(l); i<=(r); i++) 9 #define For(i,x) for (int i=h[x],k; i; i=nxt[i]) 10 using namespace std; 11 12 const int N=200010; 13 string s; 14 map<string,int>mp; 15 int n,Q,x,y,tot,skip,cnt,rts,id[N],fa[N],rt[N],ans[N],sz[N]; 16 int son[N],d[N],h[N],to[N<<1],nxt[N<<1]; 17 set<int>S[N]; 18 struct P{ int x,y; }; 19 vector<P>ve[N]; 20 void add(int u,int v){ to[++cnt]=v; nxt[cnt]=h[u]; h[u]=cnt; } 21 22 void dfs(int x){ 23 d[x]=d[fa[x]]+1; sz[x]=1; 24 For(i,x){ 25 dfs(k=to[i]); sz[x]+=sz[k]; 26 if (sz[k]>sz[son[x]]) son[x]=k; 27 } 28 } 29 30 void get(int x,int op){ 31 if (op) S[d[x]].insert(id[x]); else S[d[x]].erase(id[x]); 32 For(i,x) if ((k=to[i])!=skip) get(k,op); 33 } 34 35 void work(int x,int op){ 36 For(i,x) if ((k=to[i])!=son[x]) work(k,1); 37 if (son[x]) work(son[x],0),skip=son[x]; 38 get(x,1); skip=0; int ed=ve[x].size()-1; 39 rep(i,0,ed) ans[ve[x][i].x]=S[d[x]+ve[x][i].y].size(); 40 if (op) get(x,0); 41 } 42 43 int main(){ 44 scanf("%d",&n); 45 rep(i,1,n){ 46 cin>>s>>fa[i]; 47 if (mp.find(s)!=mp.end()) id[i]=mp[s]; else mp[s]=id[i]=++tot; 48 if (!fa[i]) rt[++rts]=i; else add(fa[i],i); 49 } 50 scanf("%d",&Q); 51 rep(i,1,Q) scanf("%d%d",&x,&y),ve[x].push_back((P){i,y}); 52 rep(i,1,rts) dfs(rt[i]),work(rt[i],1); 53 rep(i,1,Q) printf("%d\n",ans[i]); 54 return 0; 55 }
例四:[CF741D]一棵有根树,边上有字母a~v,求每个子树中最长的边,满足这个边上的所有字母重排后可以构成回文。
很容易想到点分治,但由于是有根树,所以点分治是做不了的。
先对没给点记录w[x]表示x到根的路径的各个字母出现奇偶性,然后p[S]记录满足w[x]=S的所有点x的最大深度。
类似点分治,一棵子树一棵子树地处理,每次先对子树内每个点查找之前子树中是否有能与它拼成回文串的点与它的最大深度,再更新p[]。
注意查找和更新不能同时进行,要整棵子树都查询完毕之后再进行更新操作。注意查找与更新当前子树的根。
1 #include<cstdio> 2 #include<algorithm> 3 #define rep(i,l,r) for (int i=(l); i<=(r); i++) 4 #define For(i,x) for (int i=h[x],k; i; i=nxt[i]) 5 using namespace std; 6 7 const int N=1000010,inf=1e9; 8 char s[N]; 9 int n,cnt,d[N],fa[N],ans[N],w[N],sz[N],son[N],p[1<<23],h[N],to[N<<1],nxt[N<<1]; 10 void add(int u,int v){ to[++cnt]=v; nxt[cnt]=h[u]; h[u]=cnt; } 11 12 void dfs(int x){ 13 d[x]=d[fa[x]]+1; sz[x]=1; 14 For(i,x){ 15 k=to[i]; w[k]=w[x]^(1<<(s[k]-'a')); dfs(k); sz[x]+=sz[k]; 16 if (sz[k]>sz[son[x]]) son[x]=k; 17 } 18 } 19 20 void get(int rt,int x){ 21 ans[rt]=max(ans[rt],p[w[x]]+d[x]-2*d[rt]); 22 rep(i,0,21) ans[rt]=max(ans[rt],p[w[x]^(1<<i)]+d[x]-2*d[rt]); 23 For(i,x) get(rt,k=to[i]); 24 } 25 26 void get2(int x){ p[w[x]]=max(p[w[x]],d[x]); For(i,x) get2(k=to[i]); } 27 28 void del(int x){ p[w[x]]=-inf; For(i,x) del(k=to[i]); } 29 30 void work(int x,int op){ 31 For(i,x) if ((k=to[i])!=son[x]) work(k,1),ans[x]=max(ans[x],ans[k]); 32 if (son[x]) work(son[x],0),ans[x]=max(ans[x],ans[son[x]]); 33 For(i,x) if ((k=to[i])!=son[x]) get(x,k),get2(k); 34 ans[x]=max(ans[x],p[w[x]]-d[x]); 35 rep(i,0,21) ans[x]=max(ans[x],p[w[x]^(1<<i)]-d[x]); 36 p[w[x]]=max(p[w[x]],d[x]); 37 if (op) del(x); 38 } 39 40 int main(){ 41 freopen("741D.in","r",stdin); 42 freopen("741D.out","w",stdout); 43 scanf("%d",&n); 44 rep(i,2,n) scanf("%d %c",&fa[i],&s[i]),add(fa[i],i); 45 rep(i,0,(1<<22)-1) p[i]=-inf; 46 dfs(1); work(1,0); 47 rep(i,1,n) printf("%d ",ans[i]); 48 return 0; 49 }