====== 树链剖分 ====== ===== 树链剖分的思想及能解决的问题 ===== 树链剖分用于将树分割成若干条链的形式,以维护树上路径的信息。 具体来说,将整棵树剖分为若干条链,使它组合成线性结构,然后用其他的数据结构维护信息。 **树链剖分**(树剖/链剖)有多种形式,如**重链剖分**,**长链剖分**和用于 Link/cut Tree 的剖分(有时被称作“实链剖分”),大多数情况下(没有特别说明时),“树链剖分”都指“重链剖分”。 重链剖分可以将树上的任意一条路径划分成不超过 $O(\log n)$ 条连续的链,每条链上的点深度互不相同(即是自底向上的一条链,链上所有点的 LCA 为链的一个端点)。 重链剖分还能保证划分出的每条链上的结点 DFS 序连续,因此可以方便地用一些维护序列的数据结构(如线段树)来维护树上路径的信息。 如: - 修改**树上两点之间的路径上**所有点的值。 - 查询**树上两点之间的路径上**结点权值的**和/极值/其它(在序列上可以用数据结构维护,便于合并的信息)**。 除了配合数据结构来维护树上路径信息,树剖还可以用来 $O(\log n)$ (且常数较小)地求 LCA。在某些题目中,还可以利用其性质来灵活地运用树剖。 ===== 重链剖分 ===== 我们给出一些定义: 定义**重子结点**表示其子结点中子树最大的子结点。如果有多个子树最大的子结点,取其一。如果没有子结点,就无重子结点。 定义**轻子结点**表示剩余的所有子结点。 从这个结点到重子结点的边为**重边**。 到其他轻子结点的边为**轻边**。 若干条首尾衔接的重边构成**重链**。 把落单的结点也当作重链,那么整棵树就被剖分成若干条重链。 如图: {{https://oi-wiki.org/graph/images/hld.png| HLD}} ===== 实现 ===== 树剖的实现分两个 DFS 的过程。伪代码如下: 第一个 DFS 记录每个结点的父节点(father)、深度(deep)、子树大小(size)、重子结点(hson)。 $$ \begin{array}{l}\text{TREE-BUILD}(u,dep)\\ \begin{array}{ll} 1 & u.hson\gets 0\\ 2&u.hson.size\gets 0\\ 3 & u.deep\gets dep\\ 4&u.size\gets 1\\ 5 & \textbf{for}\ \text{each}\ u\text{'s son}\ v\\ 6 & \qquad u.size\gets u.size+\text{TREE-BUILD}(v,dep+1)\\ 7 & \qquad v.father\gets u\\ 8 & \qquad \textbf{if}\ v.size>u.hson.size\\ 9 & \qquad\qquad u.hson\gets v\\ 10 & \textbf{return}\ u.size \end{array}\end{array} $$ 第二个 DFS 记录所在链的链顶(top,应初始化为结点本身)、重边优先遍历时的 DFS 序(dfn)、DFS 序对应的结点编号(rank)。 $$ \begin{array}{l}\text{TREE-DECOMPOSITION }(u,top)\\ \begin{array}{ll} 1 & u.top\gets top\\ 2 & tot\gets tot+1\\ 3 & u.dfn\gets tot\\ 4 & rank(tot)\gets u\\ 5 & \textbf{if}\ u.hson\ \text{is not}\ 0\\ 6 & \qquad\text{TREE-DECOMPOSITION}(u.hson,top)\\ 7 & \qquad\textbf{for }\text{each }u\text{'s son }v\\ 8& \qquad\qquad\textbf{if }v\text{ is not }u.hson\\ 9 & \qquad\qquad\text{TREE-DECOMPOSITION }(v,v) \end{array}\end{array} $$ 以下为代码实现。 我们先给出一些定义: * $fa(x)$ 表示结点 $x$ 在树上的父亲。 * $dep(x)$ 表示结点 $x$ 在树上的深度。 * $siz(x)$ 表示结点 $x$ 的子树的结点个数。 * $son(x)$ 表示结点 $x$ 的**重儿子**。 * $top(x)$ 表示结点 $x$ 所在**重链**的顶部结点(深度最小)。 * $dfn(x)$ 表示结点 $x$ 的**DFS序**,也是其在线段树中的编号。 * $rnk(x)$ 表示 DFS 序所对应的结点编号,有 $rnk(dfn(x))=x$ 。 我们进行两遍 DFS 预处理出这些值,其中第一次 DFS 求出 $fa(x),dep(x),siz(x),son(x)$,第二次 DFS 求出 $top(x),dfn(x),rnk(x)$。 void dfs1(int o){ son[o]=-1; siz[o]=1; for(int j=h[o];j;j=nxt[j]){ if(!dep[p[j]]){ dep[p[j]]=dep[o]+1; fa[p[j]]=o; dfs1(p[j]); siz[o]+=siz[p[j]]; if(son[o]==-1||siz[p[j]]>siz[son[o]]) son[o]=p[j]; } } } void dfs2(int o,int t){ top[o]=t; cnt++; dfn[o]=cnt; rnk[cnt]=o; if(son[o]==-1) return; dfs2(son[o],t);// 优先对重儿子进行 DFS ,可以保证同一条重链上的点 DFS 序连续 for(int j=h[o];j;j=nxt[j]){ if(p[j]!=son[o]&&p[j]!=fa[o]) dfs2(p[j],p[j]); } } ===== 重链剖分的性质 ===== **树上每个结点都属于且仅属于一条重链**。 重链开头的结点不一定是重子结点(因为重边是对于每一个结点都有定义的)。 所有的重链将整棵树**完全剖分**。 在剖分时**优先遍历重儿子**,最后重链的 DFS 序就会是连续的。 在剖分时**重边优先遍历**,最后树的 DFN 序上,重链内的 DFN 序是连续的。按 DFN 排序后的序列即为剖分后的链。 一棵子树内的 DFN 序是连续的。 可以发现,当我们向下经过一条轻边时,所在子树的大小至少会除以二。 因此,对于树上的任意一条路径,把它拆分成从 LCA 分别向两边往下走,分别最多走 $O(\log n)$次,因此,树上的每条路径都可以被拆分成不超过 $O(\log n)$条重链。 ===== 常见应用 ===== ==== 路径上维护 ==== 用树链剖分求树上两点路径权值和,伪代码如下: $$ \begin{array}{l}\text{TREE-PATH-SUM }(u,v)\\ \begin{array}{ll} 1 & tot\gets 0 \\ 2 & \textbf{while }u.top\text{ is not }v.top\\ 3 & \qquad\textbf{if }u.top.deep int lca(int u,int v){ while(top[u]!=top[v]){ if(dep[top[u]]>dep[top[v]]) u=fa[top[u]]; else v=fa[top[v]]; } return dep[u]>dep[v]?v:u; } ===== 例题 ===== [[https://www.luogu.com.cn/problem/P3384|P3384 【模板】轻重链剖分]] 参考代码: #include using namespace std; const int N=1e5+5; typedef long long ll; int n,m,root,mod; int cnt,head[N],nxt[N<<1],to[N<<1],w[N],nw[N]; int tr[N<<2],add[N<<2]; int hson[N],dfn[N],fa[N],tot,dep[N],siz[N],top[N],rk[N]; void addedge(int u,int v){ nxt[++cnt]=head[u]; head[u]=cnt; to[cnt]=v; } // segment tree inline int ls(int o){ return o<<1; } inline int rs(int o){ return o<<1|1; } void push_up(int o){ tr[o]=(tr[ls(o)]+tr[rs(o)])%mod; } void build(int o,int l,int r){ if(l==r){ tr[o]=nw[l]%mod; return; } int mid=l+r>>1; build(ls(o),l,mid); build(rs(o),mid+1,r); push_up(o); } void f(int o,int l,int r,int k){ add[o]=(add[o]+k); tr[o]=(tr[o]+(r-l+1)*k)%mod; } void push_down(int o,int l,int r){ int mid=l+r>>1; f(ls(o),l,mid,add[o]); f(rs(o),mid+1,r,add[o]); add[o]=0; } void xg(int o,int nl,int nr,int l,int r,int k){ if(nl<=l&&r<=nr){ add[o]=(add[o]+k)%mod; tr[o]=(tr[o]+(r-l+1)*k)%mod; return; } push_down(o,l,r); int mid=l+r>>1; if(nl<=mid) xg(ls(o),nl,nr,l,mid,k); if(nr>mid) xg(rs(o),nl,nr,mid+1,r,k); push_up(o); } int cx(int o,int nl,int nr,int l,int r){ if(nl<=l&&r<=nr) return tr[o]%mod; push_down(o,l,r); int ret=0; int mid=l+r>>1; if(nl<=mid) ret+=cx(ls(o),nl,nr,l,mid); if(nr>mid) ret+=cx(rs(o),nl,nr,mid+1,r); return ret%mod; } // original tree int cxRange(int x,int y){ int ans=0; while(top[x]!=top[y]){ if(dep[top[x]]dep[y]) swap(x,y); ans+=cx(1,dfn[x],dfn[y],1,n); ans%=mod; return ans; } void xgRange(int x,int y,int k){ k%=mod; while(top[x]!=top[y]){ if(dep[top[x]]dep[y]) swap(x,y); xg(1,dfn[x],dfn[y],1,n,k); } int cxSon(int x){ return cx(1,dfn[x],dfn[x]+siz[x]-1,1,n); } void xgSon(int x,int k){ xg(1,dfn[x],dfn[x]+siz[x]-1,1,n,k); } // dfs void dfs1(int u,int f,int deep){ dep[u]=deep; siz[u]=1; fa[u]=f; for(int i=head[u];i;i=nxt[i]){ int v=to[i]; if(v==fa[u]) continue; dfs1(v,u,deep+1); siz[u]+=siz[v]; if(hson[u]==0||siz[hson[u]] ===== 参考资料 ===== [[https://oi-wiki.org/graph/hld/|OI Wiki]]