/*参考 https://www.cnblogs.com/LoveYayoi/p/6745541.html
https://www.cnblogs.com/George1994/p/7821357.html
以及我校学长的优秀讲解。
*/
树链剖分可以将树剖分成一条一条的链,形成一条新链,然后通过一些维护线性数据的数据结构(比如线段树)来维护树上的信息,例如 树上求两点间边or点权和、边权or点权max or min、修改边or点权 等。
P.S.来自另一位大佬(LoveYayoi)的解释:给定一棵形态不变的树,要求支持查询一条链,修改一条链。
对于“给定一棵形态变化的树,要求支持查询和修改一条链”,需要用到LCT。
一、概念:
重儿子:u的所有子节点中,子树最大的一个。特别的,根节点自己是一个重儿子。
轻儿子:u的所有子节点中,除了重儿子的其他儿子。
重边:u与其重儿子所连的边。
轻边:u与其轻儿子所连的边。
重链:重边所形成的链。
轻链:其实就是轻边,因为只包含一条轻边。
dep[i]:点i在树上的深度。
siz[i]:以点i为根的子树(包括点i)的大小(点数之和)。
fa[i]:点i的父节点。
top[i]:点i所在的链的顶端。对于轻链,其顶端为点i本身。
dfn[i]:点i在DFS序中的编号。
rnk[i]:DFS中编号为i的点在树上的编号。
二、性质:
1.一棵树被剖分后所形成的重链数与轻链数不会超过logn条。
->这样每蹦一次操作一次,共操作O(重链数)=O(logn)次;每次操作的复杂度是O(logn);
总共的时间复杂度是O(qlogn^2)
2.若一条边(u,v)是一条轻边,则size(v)<size(u)/2。
->若一条边(u,v)是一条轻边,即点v是轻儿子,则点u至少有两个子节点;若点v是轻儿子,其子节点最多不会超过size(u)/2,否则v就成了重儿子。
三、代码:
(一)预处理
第一遍DFS,预处理出dep[]、fa[]、siz[]。
void dfs1(int u,int f,int d){ dep[u]=d; siz[u]=1; fa[u]=f; for(int i=head[u];i;i=e[i].nxt){ int v=e[i].to; if(v==fa[u]) continue; dfs1(v,u,d+1); siz[u]+=siz[v]; if(!son[u]||siz[v]>siz[son[u]]) son[u]=v; } }
第二遍DFS,预处理出tid[]、rnk[]、top[]。
int idx; void dfs2(int u,int tp){ dfn[u]=++idx;//u的DFS序编号为idx rnk[idx]=u;//在DFS序上编号为idx的点在树上的编号为u top[u]=tp; if(!son[u]) return; dfs2(son[u],tp);//处理重链 for(int i=head[u];i;i=e[i].nxt){ int v=e[i].to; if(v==fa[u]||v==son[u]) continue; dfs2(v,v);//处理轻链 } }
(二)树上路径问题
①LCA(u,v)
对于树上两点u与v间路径的一系列问题,可以将其拆分为u->lca(u,v)和v->lca(u,v)两个子问题。类似于倍增求LCA,树链剖分也是类似的方法。对于u和v两点的LCA,我们让所在链的深度(即dep[top[]])大的一点跳到另一点所在的链上,当两点同在一链时,深度小的一点就是LCA(u,v)。
int lca(int u,int v){ int f1=top[u],f2=top[v]; //令所在链的深度大的一点跳到另一点所在的链上 while(f1!=f2){ //令u为所在链的深度大的一点 if(dep[f1]<dep[f2]){ swap(f1,f2); swap(u,v); } u=fa[f1]; //u跳到所在链的顶端f1=top[u]的父节点,从而进入下一条重链。 f1=top[u]; //同样所在链顶端f1也要修改 } //当两点同处一链时,深度小的为LCA int lca; if(dep[u]>=dep[v]) { lca=v; } else lca=u; return lca; }
②链上点权和、点权max...
在求LCA时通过线段树随时更新ans即可。注意查询时查询的是点的DFS序。
->链上点权和
int lca_sum(int u,int v){ int ans=0; int f1=top[u],f2=top[v]; while(f1!=f2){ if(dep[f1]<dep[f2]){ swap(f1,f2); swap(u,v); } ans+=ask_sum(1,dfn[f1],dfn[u]);//线段树区间求和 u=fa[f1]; f1=top[u]; } int lca,tmp; if(dep[u]>=dep[v]) { lca=v,tmp=u; } else lca=u,tmp=v; ans+=ask_sum(1,dfn[lca],dfn[tmp]); return ans; }
->链上最大点权
int lca_max(int u,int v){ int ans=-(1<<30); int f1=top[u],f2=top[v]; while(f1!=f2){ if(dep[f1]<dep[f2]){ swap(f1,f2); swap(u,v); } ans=max(ans,ask_max(1,tid[f1],tid[u]));//线段树区间max u=fa[f1]; f1=top[u]; } int lca,tmp; if(dep[u]>=dep[v]) { lca=v,tmp=u; } else lca=u,tmp=v; ans=max(ans,ask_max(1,tid[lca],tid[tmp])); return ans; }
P.S.
1.求链上边权和、链上边权max...时,可以将边权下放为点权。此时注意查询的边界问题:由于u向上蹦时,会从u蹦到fa[top[u]]。事实上我们只经过了top[u]~u之间的边,因此查询的边界为dfn[top[u]]~dfn[u],而不包括dfn[fa[top[u]]]的信息,因为没有经过其保存的边。
2.建树时,已知的是线段树上的编号,即DFS序编号。所以需要将线段树编号转化为树上的编号,例如tree[u].sum=val[rnk[l]],其中val[]表示点权,rnk[l]表示DFS编号为l的点在树上的编号为dfn[l]。
原文:https://www.cnblogs.com/Loi-Brilliant/p/9126264.html