一种树的处理办法,在进行过树链剖分过后,书上任意一条链上的节点都可以用$O(logn)$个连续的区间表示
一个点的$size$为一个点子树中包括该节点本身的节点个数。
一个点的重儿子为一个点的儿子中,$size$最大的一个。如果有多个,那么就任取一个作为重儿子。
我们定义重边为一个点和其重儿子之间的边,而轻边为除了重边之外的其他边。
重边构成的链为重链。
我们定义一个点的$top$为该点所在的重链深度最小的点。
所谓的树链剖分其实也就是将一颗树的所有边分为轻链和重链两个部分。剖分效果如图。
树链剖分要经过两次深搜。 第一次深搜以某个节点为根开始深搜得到每个点的重儿子,具体流程为统计每个儿子的$size$,保留其中$size$最大的为重儿子,与此同时确定一的点的父亲,$size$以及其深度。
void dfs1(int u) { siz[u]=1; for(int i=head[u];~i;i=Edges[i].next) { int v=Edges[i].to; if(v==father[u])continue; father[v]=u;dep[v]=dep[u]+1; dfs1(v);siz[u]+=siz[v]; if(siz[v]>siz[son[u]])son[u]=v; } }
第二次深搜要维护每个点的$top$,首先根节点的$top$是其本身,对于一个点,如果该点有重儿子,那么优先索其重儿子,而其重儿子的$top$也就是该点的top,对于其他儿子,其$top$就是儿子自身。
void dfs2(int u,int t) { top[u]=t; if(son[u])dfs2(son[u],t); for(int i=head[u];~i;i=Edges[i].next) { int v=Edges[i].to; if(v==father[u]||v==son[u])continue; dfs2(v,v); } }
那么此时,我们对一条链$(x,y)$的询问,我们假设$x$的$top$较深,那么我们就让$x$跳转到$x$的父亲,并处理$x$到$x$的$top$之间这一段重链。一直重复这个过程直到$x$和$y$的$top$相同,然后对当前两点间的重链进行处理。
示例代码如下。
void update(int x,int y) { while (top[x]!=top[y]) { if (dep[top[x]]<dep[top[y]])swap(x,y); query(num[top[x]],num[x]); x = father[top[x]]; } if (dep[x]<dep[y])swap(x,y); query(num[y],num[x]); }
那么这样剖分并查询为什么时间复杂度是$O(logn)$的呢,首先dfs的时候优先处理每个点的重儿子,那么一条重链在dfs序列中式一段连续的区间。我们在查询的时候,每次跳到$top$的父亲都会走一条轻边,可以发现$fa_{top_x}$的$size$至少是$top_x$的$size$的二倍,那么我们最多跳转了$log(n)$次就会结束查询。
通常树链剖分算法会和线段树相结合。
$n$个节点的无根树,$m$次操作,可以给$a,b$路径上的节点染色,或是查询$a,b$路径上的颜色段数。如1122222111
是$3$段。$n,m\le 10^5$
题解:显然树剖,用线段树维护区间的颜色段数以及记录左右两端的颜色便于合并。
code:
给定$N$个节点的树,每个节点为一个城市,有自己的宗教和评级$c_i,w_i$,旅行者只会留宿信仰相同的城市,并且旅行终点是他们信仰相同的城市。有四种操作,CC x c
:城市$x$的居民全体改信了$c$教;CW x w
:城市$x$的评级调整为$w$;QS x y
:查询$x$到$y$旅行途中留宿过的城市的评级总和;QM x y
:查询$x$到$y$旅行途中留宿过城市的评级最大值。$N,Q\le10^5,C\le10^5$
题解:没有宗教的话就是一道模板题。不过有了宗教我们也可以对每个宗教建一棵线段树,空间问题动态开点即可。
code:
感觉这两道题有一点点共性但又不是显式的,无论如何挺值得两连做的。建议思考,仿佛有智力开发的效果
给出$n$个点的有根树,$q$次询问,每次询问给出$l,r,z$,求$\sum\limits_{l\le i\le r}\text{dep}[\text{LCA}(i,z)]$。$1\le n,q\le50000$
题解:定义询问$(x,z)$为$\sum\limits_{1\le i\le x}\text{dep}[\text{LCA}(i,z)]$,则可以将题中询问拆为询问$(r,z)$和$(l-1,z)$的差。于是可以离线,从$1$到$n$逐个加入,加入时将该节点到根节点路径上点的值各加一,线段树维护。而对于每个$z$可以在加入$x$刚好完成时查询$z$到根节点路径上点的值来得到答案。
code:
给出$n$个点的带权树,节点$i$的妖怪有年龄$x_i$。$Q$个询问,给出$u,l,r$询问年龄在$[l,r]$的妖怪到$u$点的距离之和,强制在线。$n\le150000,Q\le200000$,年龄上限$A\le10^9$。
题解:首先点$u,v$间的距离可以转换为$\text{dis}(u,\text{root})+\text{dis}(v,\text{root})-\text{dis}(\text{lca}(u,v),\text{root})$。对于每个询问$\text{dis}(u,\text{root})$和$\sum\limits_{x_v\in[l,r]}\text{dis}(v,\text{root})$都是可以通过提前处理快速得到的,那么剩下需要的就是$\sum\limits_{x_v\in[l,r]}\text{dis}(\text{lca}(u,v),\text{root})$。这里就是和上一题有点像的地方了,我们可以将年龄离散化然后维护一个主席树从小到大按顺序加入结点,加入时将该结点到根的边权加到主席树里,之后对于每个询问就也可以直接在$l-1$和$r$查询$u$到根得到结果。但主席树的具体过程是有一点玄妙的,还有$\text{lazy tag}$什么的想象不出可以见代码,$w[i]$维护主席树中结点的父边边权的前缀和。
code: