题目链接:(CF)http://codeforces.com/contest/600/problem/E
(洛谷)https://www.luogu.com.cn/problem/CF600E
参考博客:(树链剖分)https://www.cnblogs.com/zwfymqz/p/8094500.html#_label3_0
(dsu of tree)https://www.cnblogs.com/zcysky/p/6822395.html
首先对于树链剖分来说,他有两个性质。
性质1:如果边(u,v)为轻边,那么Size(v)≤Size(u)/2。 性质2:树中任意两个节点之间的路径中轻边的条数不会超过logn,重路径的数目不会超过logn 性质2的重路径指的是一段连续的重边所构成的路径。 对于性质2我只有一个比较模糊的理解: 首先因为有性质1的存在,我们可以得出“树中任意两个节点之间的路径上轻边的条数不会超过logn”这个结论。 因为假设我们要计算点u到点v之间轻边的数量,那么当u是根节点,并且v是叶子节点的时候这个值应该才是最大的。
u到v的路径上每增加一条轻边,即u向下走一层走到u’,uu’这条边是轻边,那么u’为根节点的子树的大小一定会小于等于size(u)/2,
这样一直增加轻边,一直要让最初值size(u)除以2,我们大概就可以得出性质2的前半部分。 而如果性质2的前半部分是对的,我们实际上就可以直接推出后半部分,即“树中任意两个节点之间重路径的数目不会超过logn”,
因为轻边和重路径是相互连接的,可以理解为重路径被两条轻边竖直方向上“夹住”,当然,除去一些特殊情况。
所以重路径的数量和轻边的数量大体上是差不多的,这样我们就可以大致理解一下性质2了。
这道题的题意(来自洛谷):一棵树有n个结点,每个结点都是一种颜色,每个颜色有一个编号,求树中每个子树的最多的颜色编号的和。
思路:先dfs求出每一个点的重儿子,然后dfs计算每一个点所在子树最多的颜色编号的和:对于每一个子树根节点来说,我们先计算它每一个轻儿子子树中最多的颜色编号的和,每计算完一个轻儿子的值,我们就消除这个轻儿子给子树根节点带来的影响,最后我们再计算重儿子的值,这里计算完之后我们不消除重儿子所带来的影响,因为我们要把这个状态保存,在计算子树根节点的值的时候就可以不用重复计算一次它的重儿子所在子树的的值了。计算每一个点中最多的颜色编号的和就直接遍历一次它的子树,只是跳过重儿子所在的子树而已,因为之前已经计算出来了,并且没有消除它所带来的影响。说的不是很清楚,直接看代码:
时间复杂度是nlogn
#include<iostream> #include<cstring> #include<algorithm> #include<queue> #include<map> #include<stack> #include<cmath> #include<vector> #include<set> #include<cstdio> #include<string> #include<deque> using namespace std; typedef long long ll; #define eps 1e-8 #define INF 0x3f3f3f3f #define maxn 100005 int head[maxn],cnt,n,m,k,t; int color[maxn]; int tot[maxn],son[maxn]; int num[maxn],Max,Son;//Max存子树中出现颜色出现的最多次数,Son存的是子树的重儿子 ll sum,ans[maxn];//这里注意要用long long struct node{ int v,next; }edge[maxn*2]; void init(){ memset(head,-1,sizeof(head)); memset(son,0,sizeof(son)); memset(num,0,sizeof(num)); cnt=Max=sum=0; } void add(int u,int v){ edge[++cnt].v=v; edge[cnt].next=head[u]; head[u]=cnt; } void dfs(int u,int pre){//算出树上每一个点的重儿子 tot[u]=1; int mx=-1; for(int i=head[u];i!=-1;i=edge[i].next){ int v=edge[i].v; if(v==pre) continue; dfs(v,u); tot[u]+=tot[v]; if(mx<tot[v]){ mx=tot[v]; son[u]=v; } } } void add(int u,int pre,int op){//对一棵子树上的点做加减操作,在这里面加操作和减操作是不一样的, //减操作是所有点都减一次,加操作是跳过重儿子为根节点的那颗子树 num[color[u]]+=op; int value=num[color[u]]; if(value>Max){ Max=value; sum=color[u]; }else if(value==Max){ sum+=color[u]; } for(int i=head[u];i!=-1;i=edge[i].next){ int v=edge[i].v; if(v==pre||v==Son&&op==1) //这里对于加操作来说,重儿子所在的子树是不用加的 continue; //因为之前并没有消除重儿子所在子树的影响 add(v,u,op); } } void dfs2(int u,int pre,int op){ for(int i=head[u];i!=-1;i=edge[i].next){ int v=edge[i].v; if(v==pre||v==son[u]) continue; dfs2(v,u,0); } if(son[u]){ dfs2(son[u],u,1); } Son=son[u]; //更新重儿子,因为在给子树做加操作的时候可以跳过重儿子所在子树 add(u,pre,1);//这里的加操作会跳过重儿子所在的重边,所以只会对所有的轻儿子所在子树做加操作, //又因为每两个点之间的轻边数量不会超过logn,所以对于每一个点来说,它做加操作的次数是不超过logn的,和下面的减操作相似 ans[u]=sum; Son=0; //清空重儿子 if(op==0){ //消除轻儿子所在子树的影响,因为每两个点之间的轻边数量不会超过logn,所以对于每一个点来说, //它做减法的次数不会超过logn,因为只有它的祖先节点在轻边上的时候它才会做减操作 add(u,pre,-1); sum=0; Max=0; } } int main() { while(scanf("%d",&n)!=EOF){ init(); int u,v; for(int i=1;i<=n;i++){ scanf("%d",&color[i]); } for(int i=0;i<n-1;i++){ scanf("%d%d",&u,&v); add(u,v); add(v,u); } dfs(1,0); dfs2(1,0,1); for(int i=1;i<=n;i++){ if(i!=n) printf("%lld ",ans[i]); else printf("%lld\n",ans[i]); } } return 0; }