前言:树上启发式合并(DSU on Tree),是一个在O(nlogn) 时间内解决许多树上问题的有力算法,其对于树上离线问题的处理速度大于等于其他的算法,且更容易理解(个人认为处理与子树的关系牵涉很多)。具体思路大概就是先像树链剖分那样找到每个结点的重儿子,然后把所有轻儿子的贡献合并于重儿子(比较抽象吧~),当前结点操作完毕之后,再看如果当前结点是其父亲结点的一个轻儿子,那么该轻儿子贡献全部置0。对于重儿子的贡献保留。至于时间复杂度为什么是O(logn),我也不太会证明,但是肯定比在线暴力对每个儿子操作其全部结点快,因为首先Dsu On Tree是个离线算法,你每次对当前结点的操作,实际是只要处理轻儿子结点(重儿子结点贡献已经有了),这么来看你就较常规方法少了再遍历重儿子的那一步,时间复杂度自然而然就降了。
我觉得理解一个算法的,重要的一步是训练刷题+总结,所以我附上一些刷题题单,前面几个题会简单一些(CF2300分左右),最后一个题有点难(CF2900分)
还有一道题是Wannafly Camp Day2E 阔立梯的树,我之前写过题解,是一个很不错的题:https://www.cnblogs.com/Anonytt/p/13080989.html
接下来我一道题一道题地写一下解析叭:
A:Codeforces-600E(Lomsat Gelral)
题意:对于每个子树,输出子树中出现次数最多的节点编号之和。(次数最多的编号有多个节点都要统计进去)
解法:这是一道比较模板的树上启发式合并题,每一次count,统计当前颜色col[u]出现的次数即cnt[col[u]]。如果其次数大于所有颜色中出现次数最多颜色的大小maxc,即把maxc更新成当前结点的颜色大小cnt[col[u]],同时更新sum。另外该题要求如果子树中出现颜色次数一样,那么和就等于这些相同颜色值大小和,所以你还需要特判更新一波。
#include#define ll long long #define rep(i,a,n) for(int i=a;i<=n;i++) #define per(i,n,a) for(int i=n;i>=a;i--) #define endl '\n' #define mem(a,b) memset(a,b,sizeof(a)) #define IO ios::sync_with_stdio(false);cin.tie(0); using namespace std; const int INF=0x3f3f3f3f; const ll inf=0x3f3f3f3f3f3f3f3f; const int mod=1e9+7; const int maxn=1e5+5; int tot,head[maxn]; struct E{ int to,next; }edge[maxn<<1]; void add(int u,int v){ edge[tot].to=v; edge[tot].next=head[u]; head[u]=tot++; } int col[maxn],cnt[maxn]; int siz[maxn],son[maxn]; void dfs(int u,int f){ siz[u]=1; for(int i=head[u];i!=-1;i=edge[i].next){ int v=edge[i].to; if(v==f) continue; dfs(v,u); siz[u]+=siz[v]; if(siz[v]>siz[son[u]]){ son[u]=v; } } } ll ans[maxn],sum; int flag,maxc; void count(int u,int f,int val){ cnt[col[u]]+=val; if(cnt[col[u]]>maxc){ maxc=cnt[col[u]]; sum=col[u]; } else if(cnt[col[u]]==maxc) sum+=col[u]; for(int i=head[u];i!=-1;i=edge[i].next){ int v=edge[i].to; if(v==f||v==flag) continue; count(v,u,val); } } void dfs(int u,int f,bool keep){ for(int i=head[u];i!=-1;i=edge[i].next){ int v=edge[i].to; if(v==f||v==son[u]) continue; dfs(v,u,false); } if(son[u]){ dfs(son[u],u,true); flag=son[u]; } count(u,f,1); flag=0; ans[u]=sum; if(!keep){ count(u,f,-1); sum=maxc=0; } } int main(){ int n;scanf("%d",&n); mem(head,-1); rep(i,1,n) scanf("%d",&col[i]); rep(i,1,n-1){ int u,v;scanf("%d%d",&u,&v); add(u,v);add(v,u); } dfs(1,0); dfs(1,0,0); rep(i,1,n) printf("%lld ",ans[i]); puts(""); }
B:Codeforces-246E(Blood Cousins Return)
题意:给定一棵树,已知每个节点的父亲节点,和每个节点上的名字(字符串),对于每个询问vi,ki,求子树vi内v的kth层儿子(子树内比子树根深度大k层的部分)中不重复名字的个数。
解法:因为有可能是森林,所以你需要统计一共有多少个树rootcnt,并记录每个数的根节点root[i](判断其是否为某个数根节点只需要看它父亲是不是0就行啦),利用map离散化一下,就把名字当成颜色好了。同时count的时候也要注意某子树如果当前结点为u当前层中该颜色即col[u]是否在其他子树出现过,即也用map来记录一下,如果没有那ok,如果有的话,当前层次的颜色你就不需要再更新啦!另外值得一提的是:对于结点询问,如果只给你某个结点的v和第几层d,那么可以用vector
#include#define ll long long #define rep(i,a,n) for(int i=a;i<=n;i++) #define per(i,n,a) for(int i=n;i>=a;i--) #define endl '\n' #define eps 0.000000001 #define mem(a,b) memset(a,b,sizeof(a)) #define IO ios::sync_with_stdio(false);cin.tie(0); using namespace std; const int INF=0x3f3f3f3f; const ll inf=0x3f3f3f3f3f3f3f3f; const int mod=1e9+7; const int maxn=1e5+5; int tot,head[maxn]; struct E{ int to,next; }edge[maxn<<1]; void add(int u,int v){ edge[tot].to=v; edge[tot].next=head[u]; head[u]=tot++; } int n;map<string,int> mp;int c[maxn],root[maxn]; vector int,int> >q[maxn]; int siz[maxn],son[maxn],dep[maxn]; void dfs1(int u,int f){ dep[u]=dep[f]+1; siz[u]=1; for(int i=head[u];i!=-1;i=edge[i].next){ int v=edge[i].to; if(v==f) continue; dfs1(v,u); siz[u]+=siz[v]; if(siz[v]>siz[son[u]]) son[u]=v; } } int flag,ans[maxn]; int cnt[maxn]; map int,int>,int> pk; void count(int u,int f,int val){ if(val==1&&!pk[{dep[u],c[u]}]) cnt[dep[u]]+=val,pk[{dep[u],c[u]}]+=val; if(val==-1&&pk[{dep[u],c[u]}]) cnt[dep[u]]+=val,pk[{dep[u],c[u]}]+=val; for(int i=head[u];i!=-1;i=edge[i].next){ int v=edge[i].to; if(v==f||v==flag) continue; count(v,u,val); } } void dfs(int u,int f,int keep){ for(int i=head[u];i!=-1;i=edge[i].next){ int v=edge[i].to; if(v==f||v==son[u]) continue; dfs(v,u,0); } if(son[u]){ dfs(son[u],u,1); flag=son[u]; } count(u,f,1); for(int i=0;i ){ int id=q[u][i].first; int d=q[u][i].second; ans[id]=cnt[dep[u]+d]; } flag=0; if(!keep){ count(u,f,-1); } } int main(){ cin>>n;mem(head,-1); int cntcolor=0;int cntroot=0; rep(i,1,n){ string s;int x;cin>>s>>x; if(!mp[s]) mp[s]=++cntcolor; c[i]=mp[s]; if(!x){ ++cntroot; root[cntroot]=i; }else{ add(x,i);add(i,x); } } int m;cin>>m; rep(i,1,m){ int u,d;cin>>u>>d; q[u].push_back({i,d}); } rep(i,1,cntroot){ dfs1(root[i],0); } rep(i,1,cntroot){ dfs(root[i],0,0); } rep(i,1,m){ cout<endl; } }
C: Codeforces-1009F(Dominant Indices)
题意:给定一棵以 1 为根,n 个节点的树。设 d(u,x)为 u 子树中到 u 距离为 x 的节点数。对于每个点,求一个最小的 k,使得 d(u,k) 最大。
解法:这个其实也挺简单的,同样是记录每一层该颜色出现的次数,若该颜色次数大于等于(Hint!!)最大值就进行更新maxx和loc,你需要注意如果cnt[col[u]]和maxx一致,要把loc更新成最近的dep[u],我就是这里没注意wa了一发,其实样例就能检测出来,如果是树链的情况的话。
#include#define ll long long #define rep(i,a,n) for(int i=a;i<=n;i++) #define per(i,n,a) for(int i=n;i>=a;i--) #define endl '\n' #define eps 0.000000001 #define pb push_back #define mem(a,b) memset(a,b,sizeof(a)) #define IO ios::sync_with_stdio(false);cin.tie(0); using namespace std; const int INF=0x3f3f3f3f; const ll inf=0x3f3f3f3f3f3f3f3f; const int mod=1e9+7; const int maxn=1e6+5; int tot,head[maxn]; struct E{ int to,next; }edge[maxn<<1]; void add(int u,int v){ edge[tot].to=v; edge[tot].next=head[u]; head[u]=tot++; } int n; int dep[maxn],siz[maxn],son[maxn]; void dfs1(int u,int f){ siz[u]=1; dep[u]=dep[f]+1; for(int i=head[u];i!=-1;i=edge[i].next){ int v=edge[i].to; if(v==f) continue; dfs1(v,u); siz[u]+=siz[v]; if(siz[v]>siz[son[u]]) son[u]=v; } } int flag,cnt[maxn],maxx,loc,ans[maxn]; void count(int u,int f,int val){ cnt[dep[u]]+=val; /* if(val>0&&cnt[dep[u]]>maxx){ maxx=cnt[dep[u]]; loc=dep[u]; } */ if(val>0&&cnt[dep[u]]>=maxx){ if(cnt[dep[u]]>maxx) maxx=cnt[dep[u]],loc=dep[u]; else if(cnt[dep[u]]==maxx&&dep[u]<loc){ loc=dep[u]; } } for(int i=head[u];i!=-1;i=edge[i].next){ int v=edge[i].to; if(v==f||v==flag) continue; count(v,u,val); } } void dfs(int u,int f,int keep){ for(int i=head[u];i!=-1;i=edge[i].next){ int v=edge[i].to; if(v==f||v==son[u]) continue; dfs(v,u,0); } if(son[u]){ dfs(son[u],u,1); flag=son[u]; } count(u,f,1); ans[u]=loc-dep[u]; flag=0; if(!keep){ count(u,f,-1); maxx=0;loc=INF; } } int main(){ cin>>n;mem(head,-1);maxx=0;loc=INF; rep(i,1,n-1){ int u,v;cin>>u>>v; add(u,v);add(v,u); } dfs1(1,0); dfs(1,0,0); rep(i,1,n){ cout< endl; } }
D:Codeforces-375D(Tree and Queries)
题意:现有一棵 n 个点的树,点的编号从 1 起,树以 1 为根,每个点 i 都一个颜色 ci,接下来有 m 个询问,每次询问以 vj为根的子树中,求有多少种颜色,这些颜色在子树中出现的次数至少为 kj。
解法:直接统计就行,注意如果val是正的就{cnt[c[u]]+=val;vis[cnt[c[u]]]+=val;},否则{vis[cnt[c[u]]]+=val;cnt[c[u]]+=val;}
#include#define ll long long #define rep(i,a,n) for(int i=a;i<=n;i++) #define per(i,n,a) for(int i=n;i>=a;i--) #define endl '\n' #define eps 0.000000001 #define pb push_back #define mem(a,b) memset(a,b,sizeof(a)) #define IO ios::sync_with_stdio(false);cin.tie(0); using namespace std; const int INF=0x3f3f3f3f; const ll inf=0x3f3f3f3f3f3f3f3f; const int mod=1e9+7; const int maxn=1e5+5; int tot,head[maxn]; struct E{ int to,next; }edge[maxn<<1]; void add(int u,int v){ edge[tot].to=v; edge[tot].next=head[u]; head[u]=tot++; } int n,m,c[maxn]; vector int,int> >q[maxn]; int siz[maxn],son[maxn],dep[maxn]; void dfs1(int u,int f){ dep[u]=dep[f]+1; siz[u]=1; for(int i=head[u];i!=-1;i=edge[i].next){ int v=edge[i].to; if(v==f) continue; dfs1(v,u); siz[u]+=siz[v]; if(siz[v]>siz[son[u]]) son[u]=v; } } int flag,cnt[maxn],vis[maxn],ans[maxn]; void count(int u,int f,int val){ if(val==1) {cnt[c[u]]+=val;vis[cnt[c[u]]]+=val;} else {vis[cnt[c[u]]]+=val;cnt[c[u]]+=val;} for(int i=head[u];i!=-1;i=edge[i].next){ int v=edge[i].to; if(v==f||v==flag) continue; count(v,u,val); } } void dfs(int u,int f,int keep){ for(int i=head[u];i!=-1;i=edge[i].next){ int v=edge[i].to; if(v==f||v==son[u]) continue; dfs(v,u,0); } if(son[u]){ dfs(son[u],u,1); flag=son[u]; } count(u,f,1); for(int i=0;i ){ int id=q[u][i].first; int lim=q[u][i].second; ans[id]=vis[lim]; } flag=0; if(!keep){ count(u,f,-1); } } int main(){ scanf("%d%d",&n,&m);mem(head,-1); rep(i,1,n) scanf("%d",&c[i]); rep(i,1,n-1){ int u,v;scanf("%d%d",&u,&v); add(u,v);add(v,u); } rep(i,1,m){ int x,k;scanf("%d%d",&x,&k); q[x].pb({i,k}); } dfs1(1,0); dfs(1,0,0); rep(i,1,m){ cout<endl; } }
E:Codeforces-570D(Tree Requests)
题意:一棵以1为根的树,每个节点上都有1个字母,有m个询问。每次询问v对应的子树中,深度为h的这层节点的字母,能否打乱重排组成回文串。根的深度为1,每个点的深度为到根的距离。
解法:因为有26个字母,其实看做只有26个颜色,你每次count的时候统计cnt[dep[u]][c[u]],c[u]数组大小开到26就行了,dep[u]数组开到5e5,所以不会炸数组,看是否是回文,你只需要统计该层中所有颜色%2后的值num,并统计num的和sum,若sum=0则说明是一个全是偶数的回文串,若sum=1说明这些结点中有一个颜色是单数的,当然这也是回文串的范畴(放在最中间),但是若sum>1了就不行,因为这样就不可构成回文串了。
#include#define ll long long #define rep(i,a,n) for(int i=a;i<=n;i++) #define per(i,n,a) for(int i=n;i>=a;i--) #define endl '\n' #define eps 0.000000001 #define mem(a,b) memset(a,b,sizeof(a)) #define IO ios::sync_with_stdio(false);cin.tie(0); using namespace std; const int INF=0x3f3f3f3f; const ll inf=0x3f3f3f3f3f3f3f3f; const int mod=1e9+7; const int maxn=5e5+5; int tot,head[maxn]; struct E{ int to,next; }edge[maxn<<1]; void add(int u,int v){ edge[tot].to=v; edge[tot].next=head[u]; head[u]=tot++; } int n,m,c[maxn],ans[maxn]; vector int,int> >q[maxn]; int siz[maxn],son[maxn],dep[maxn]; void dfs1(int u,int f){ siz[u]=1;dep[u]=dep[f]+1; for(int i=head[u];i!=-1;i=edge[i].next){ int v=edge[i].to; if(v==f) continue; dfs1(v,u); siz[u]+=siz[v]; if(siz[v]>siz[son[u]]) son[u]=v; } } int flag,cnt[maxn][30]; void count(int u,int f,int val){ cnt[dep[u]][c[u]]+=val; for(int i=head[u];i!=-1;i=edge[i].next){ int v=edge[i].to; if(v==f||v==flag) continue; count(v,u,val); } } void dfs(int u,int f,int keep){ for(int i=head[u];i!=-1;i=edge[i].next){ int v=edge[i].to; if(v==f||v==son[u]) continue; dfs(v,u,0); } if(son[u]){ dfs(son[u],u,1); flag=son[u]; } count(u,f,1); for(int i=0;i ){ int id=q[u][i].first; int d=q[u][i].second; int num=0; rep(j,1,26){ if(cnt[d][j]&1) num++; } ans[id]=num>1?0:1; } flag=0; if(!keep){ count(u,f,-1); } } int main(){ scanf("%d%d",&n,&m);mem(head,-1); for(int i=2;i<=n;i++){ int u;scanf("%d",&u); add(u,i);add(i,u); } string s;cin>>s;s=" "+s; rep(i,1,n) c[i]=s[i]-'a'+1; dfs1(1,0); rep(i,1,m){ int x,y;scanf("%d%d",&x,&y); q[x].push_back(make_pair(i,y)); } dfs(1,0,0); rep(i,1,m){ if(ans[i]) puts("Yes"); else puts("No"); } }
F:Codeforces-208E(Blood Counsins)
题意:在一个森林中,如果两个节点a和b向上的第p个祖先相同,就称他们为p代表亲。(跟日常生活中有所不同,p不一定是他们的最近公共祖先)。 给出一些询问,问v的p代表亲的数量。
解法:这里就是反向思维来思考了,即求每个节点u的子树中,距离u节点距离为p的结点数量(记得最后减1即删去本身哦~),这里就用到了倍增思想来揪出距离结点a长度为p的祖先是谁。
#include#define ll long long #define rep(i,a,n) for(int i=a;i<=n;i++) #define per(i,n,a) for(int i=n;i>=a;i--) #define endl '\n' #define eps 0.000000001 #define mem(a,b) memset(a,b,sizeof(a)) #define IO ios::sync_with_stdio(false);cin.tie(0); using namespace std; const int INF=0x3f3f3f3f; const ll inf=0x3f3f3f3f3f3f3f3f; const int mod=1e9+7; const int maxn=1e5+5; int tot,head[maxn]; struct E{ int to,next; }edge[maxn<<1]; void add(int u,int v){ edge[tot].to=v; edge[tot].next=head[u]; head[u]=tot++; } int n,fa[maxn][40],root[maxn]; void bz(){ for(int j=1;j<=30;j++){ for(int i=1;i<=n;i++){ fa[i][j]=fa[fa[i][j-1]][j-1]; } } } int siz[maxn],son[maxn],dep[maxn]; void dfs1(int u,int f){ fa[u][0]=f; dep[u]=dep[f]+1; siz[u]=1; //rep(i,1,20) fa[u][i]=fa[fa[u][i-1]][i-1]; for(int i=head[u];i!=-1;i=edge[i].next){ int v=edge[i].to; if(v==f) continue; dfs1(v,u); siz[u]+=siz[v]; if(siz[v]>siz[son[u]]) son[u]=v; } } vector int,int> >q[maxn]; int flag,cnt[maxn],ans[maxn]; void count(int u,int f,int val){ cnt[dep[u]]+=val; for(int i=head[u];i!=-1;i=edge[i].next){ int v=edge[i].to; if(v==f||v==flag) continue; count(v,u,val); } } void dfs(int u,int f,int keep){ for(int i=head[u];i!=-1;i=edge[i].next){ int v=edge[i].to; if(v==f||v==son[u]) continue; dfs(v,u,0); } if(son[u]){ dfs(son[u],u,1); flag=son[u]; } count(u,f,1); for(int i=0;i ){ int id=q[u][i].first; int d=q[u][i].second; ans[id]=cnt[d]-1; } flag=0; if(!keep){ count(u,f,-1); } } int main(){ cin>>n;mem(head,-1); int rootcnt=0; rep(i,1,n){ int x;cin>>x; if(!x) root[++rootcnt]=i; else{ add(x,i);add(i,x); } } rep(i,1,rootcnt) dfs1(root[i],0); bz(); int m;cin>>m; rep(i,1,m){ int u,p;cin>>u>>p; int d=dep[u]; for(int j=20;j>=0;j--){ if(p&(1<fa[u][j]; } q[u].push_back({i,d}); } rep(i,1,rootcnt) dfs(root[i],0,0); rep(i,1,m){ cout< " "; } cout<<endl; }
G:Codeforces-741D(Arpa’s letter-marked tree and Mehrdad’s Dokhtar-kosh paths)
题意(这是算法发明者专门为这个算法量身订造的题目哦):一棵根为1 的树,每条边上有一个字符(a-v共22种)。 一条简单路径被称为Dokhtar-kosh当且仅当路径上的字符经过重新排序后可以变成一个回文串。 求每个子树中最长的Dokhtar-kosh路径的长度。给你n个点构成的一棵树,树里面的每一条边有一个权值,求出每个子树里面能通过重排构成回文串的最大路径长度.
解法:首先我们需要从一个点来入手,容易发现字符只有22种,比较少,而满足重排后能构成回文串的条件是:每个字符出现次数都为偶数或出现次数为奇数的字符仅有一个。因此我们可以考虑将其压缩成0表示出现次数为偶数,1表示出现次数为奇数。而对于一个路径上的字符,我们只需考虑每个字符的出现次数,这样我们巧妙地把问题状压了一下,用一个整数就可以表达出所有符合条件的状态:0 或者 (1<
#include