树链剖分总结
树链剖分总结
参考自:OI Wiki
树链剖分
树链剖分用于将树分割成若干条链的形式,以维护树上路径的信息。
具体来说,将整棵树剖分为若干条链,使它组合成线性结构,然后用其他的数据结构维护信息。
树链剖分(树剖/链剖)有多种形式,如 重链剖分,长链剖分 和用于 Link/cut Tree 的剖分(有时被称作“实链剖分”),大多数情况下(没有特别说明时),“树链剖分”都指“重链剖分”。
重链剖分可以将树上的任意一条路径划分成不超过 条连续的链,每条链上的点深度互不相同(即是自底向上的一条链,链上所有点的 LCA 为链的一个端点)。
重链剖分还能保证划分出的每条链上的节点 DFS 序连续,因此可以方便地用一些维护序列的数据结构(如线段树)来维护树上路径的信息。
如:
- 修改 树上两点之间的路径上 所有点的值。
- 查询 树上两点之间的路径上 节点权值的 和/极值/其它(在序列上可以用数据结构维护,便于合并的信息)。
除了配合数据结构来维护树上路径信息,树剖还可以用来 O(log n)(且常数较小)地求 LCA。在某些题目中,还可以利用其性质来灵活地运用树剖。
重链剖分
我们给出一些定义:
定义 重子节点 表示其子节点中子树最大的子结点。如果有多个子树最大的子结点,取其一。如果没有子节点,就无重子节点。
定义 轻子节点 表示剩余的所有子结点。
从这个结点到重子节点的边为 重边。
到其他轻子节点的边为 轻边。
若干条首尾衔接的重边构成 重链。
把落单的结点也当作重链,那么整棵树就被剖分成若干条重链。
如图:
实现:两次dfs
我们先给出一些定义:
- fa[] 表示节点 在树上的父亲。
- d[]表示节点 在树上的深度。
- si[]表示节点 的子树的节点个数。
- son[] 表示节点 的 重儿子。
- top[]表示节点 所在 重链 的顶部节点(深度最小)。
- id[]表示节点 的 DFS 序,也是其在线段树中的编号。
- rev[]表示 DFS 序所对应的节点编号,有 。
我们进行两遍 DFS 预处理出这些值,其中第一次 DFS 求出 fa,d,son,si,第二次 DFS 求出 top,id,rev。
void dfs1(int x,int y)//树链剖分第一次dfs,处理所有节点的深度、父节点、子树大小,并找到重儿子。 { d[x]=d[y]+1; fa[x]=y; si[x]=1; for(int i=fi[x];i;i=ne[i]) { int v=to[i]; if(v==y) continue; dfs1(v,x); si[x]+=si[v]; if(si[v]>si[son[x]]) son[x]=v; } } void dfs2(int x,int t)//树链剖分第二次dfs,处理每个节点的编号、链顶、编号对应的值 { top[x]=t; id[x]=++num; w[num]=a[x]; if(son[x]) dfs2(son[x],t); for(int i=fi[x];i;i=ne[i]) { int v=to[i]; if(v==fa[x]||v==son[x]) continue; dfs2(v,v); } }
重链剖分的性质
树上每个节点都属于且仅属于一条重链。
重链开头的结点不一定是重子节点(因为重边是对于每一个结点都有定义的)。
所有的重链将整棵树 完全剖分。
在剖分时 重边优先遍历,最后树的 DFN 序上,重链内的 DFN 序是连续的。按 DFN 排序后的序列即为剖分后的链。
一颗子树内的 DFN 序是连续的。
可以发现,当我们向下经过一条 轻边 时,所在子树的大小至少会除以二。
因此,对于树上的任意一条路径,把它拆分成从 lca分别向两边往下走,分别最多走 O(log n) 次,因此,树上的每条路径都可以被拆分成不超过 O(log n) 条重链。
常用维护:(线段树&树状数组)
此处线段树举例:
void pu(int x)//下传懒标记 { if(la[x]) { tr[x<<1].w=(tr[x<<1].w%p+la[x]*(tr[x<<1].r-tr[x<<1].l+1)%p)%p; tr[x<<1|1].w=(tr[x<<1|1].w%p+la[x]*(tr[x<<1|1].r-tr[x<<1|1].l+1)%p)%p; la[x<<1]=(la[x<<1]+la[x])%p; la[x<<1|1]=(la[x<<1|1]+la[x])%p; la[x]=0; } } void bu(int x,int l,int r)//建线段树 { tr[x].l=l,tr[x].r=r; if(l==r) { tr[x].w=w[l]; return ; } int mid=(l+r)>>1; bu(x<<1,l,mid),bu(x<<1|1,mid+1,r); tr[x].w=(tr[x<<1].w+tr[x<<1|1].w)%p; } void u(int x,int l,int r,ll k)//线段树区间修改 { pu(x); if(tr[x].l>=l&&tr[x].r<=r) { tr[x].w=(tr[x].w%p+k*(tr[x].r-tr[x].l+1)%p)%p; la[x]=(la[x]+k)%p; return; } int mid=(tr[x].l+tr[x].r)>>1; if(l<=mid) u(x<<1,l,r,k); if(r>mid) u(x<<1|1,l,r,k); tr[x].w=(tr[x<<1].w+tr[x<<1|1].w)%p; } ll q(int x,int l,int r)//线段树区间查询 { pu(x); if(tr[x].l>=l&&tr[x].r<=r) return tr[x].w; int mid=(tr[x].l+tr[x].r)>>1; ll cnt=0; if(l<=mid) cnt=(cnt+q(x<<1,l,r))%p; if(r>mid) cnt=(cnt+q(x<<1|1,l,r))%p; return cnt%p; }
建好线段树:
for(int i=1;i<=n;i++) scanf("%d",&a[i]); for(int i=1;i<n;i++) { int x,y; scanf("%d%d",&x,&y); add(x,y),add(y,x); } dfs1(root,0); dfs2(root,root); bu(1,1,n);
修改和查询区间时,不断将区间端点向一条重链靠,边靠近边修改和查询:
void up(int x,int y,ll k)// 原树上点权修改 { while(top[x]!=top[y]) { if(d[top[x]]<d[top[y]]) swap(x,y); u(1,id[top[x]],id[x],k); x=fa[top[x]]; } if(d[x]>d[y]) swap(x,y); u(1,id[x],id[y],k); } ll qu(int x,int y)// 原树上路径点权和 { ll cnt=0; while(top[x]!=top[y]) { if(d[top[x]]<d[top[y]]) swap(x,y); cnt=(cnt+q(1,id[top[x]],id[x]))%p; x=fa[top[x]]; } if(d[x]>d[y]) swap(x,y); cnt=(cnt+q(1,id[x],id[y]))%p; return cnt; }
例题:
P3384 【模板】轻重链剖分/树链剖分 - 洛谷 | 计算机科学教育新生态
代码如下:
#include<bits/stdc++.h> #define ll long long using namespace std; const int N=2e5+5; int n,m,root,p,tot,num; int a[N],fa[N],si[N],son[N],d[N],top[N]; int id[N],w[N],la[N]; int fi[N],ne[N*2],to[N*2]; struct xiao { int l,r; ll w; }tr[N*4]; void add(int x,int y)//建树 { ne[++tot]=fi[x]; fi[x]=tot; to[tot]=y; } void pu(int x)//下传懒标记 { if(la[x]) { tr[x<<1].w=(tr[x<<1].w%p+la[x]*(tr[x<<1].r-tr[x<<1].l+1)%p)%p; tr[x<<1|1].w=(tr[x<<1|1].w%p+la[x]*(tr[x<<1|1].r-tr[x<<1|1].l+1)%p)%p; la[x<<1]=(la[x<<1]+la[x])%p; la[x<<1|1]=(la[x<<1|1]+la[x])%p; la[x]=0; } } void dfs1(int x,int y)//树链剖分第一次dfs,处理所有节点的深度、父节点、子树大小,并找到重儿子。 { d[x]=d[y]+1; fa[x]=y; si[x]=1; for(int i=fi[x];i;i=ne[i]) { int v=to[i]; if(v==y) continue; dfs1(v,x); si[x]+=si[v]; if(si[v]>si[son[x]]) son[x]=v; } } void dfs2(int x,int t)//树链剖分第二次dfs,处理每个节点的编号、链顶、编号对应的值 { top[x]=t; id[x]=++num; w[num]=a[x]; if(son[x]) dfs2(son[x],t); for(int i=fi[x];i;i=ne[i]) { int v=to[i]; if(v==fa[x]||v==son[x]) continue; dfs2(v,v); } } void bu(int x,int l,int r)//建线段树 { tr[x].l=l,tr[x].r=r; if(l==r) { tr[x].w=w[l]; return ; } int mid=(l+r)>>1; bu(x<<1,l,mid),bu(x<<1|1,mid+1,r); tr[x].w=(tr[x<<1].w+tr[x<<1|1].w)%p; } void u(int x,int l,int r,ll k)//线段树区间修改 { pu(x); if(tr[x].l>=l&&tr[x].r<=r) { tr[x].w=(tr[x].w%p+k*(tr[x].r-tr[x].l+1)%p)%p; la[x]=(la[x]+k)%p; return; } int mid=(tr[x].l+tr[x].r)>>1; if(l<=mid) u(x<<1,l,r,k); if(r>mid) u(x<<1|1,l,r,k); tr[x].w=(tr[x<<1].w+tr[x<<1|1].w)%p; } void up(int x,int y,ll k)// 原树上点权修改 { while(top[x]!=top[y]) { if(d[top[x]]<d[top[y]]) swap(x,y); u(1,id[top[x]],id[x],k); x=fa[top[x]]; } if(d[x]>d[y]) swap(x,y); u(1,id[x],id[y],k); } ll q(int x,int l,int r)//线段树区间查询 { pu(x); if(tr[x].l>=l&&tr[x].r<=r) return tr[x].w; int mid=(tr[x].l+tr[x].r)>>1; ll cnt=0; if(l<=mid) cnt=(cnt+q(x<<1,l,r))%p; if(r>mid) cnt=(cnt+q(x<<1|1,l,r))%p; return cnt%p; } ll qu(int x,int y)// 原树上路径点权和 { ll cnt=0; while(top[x]!=top[y]) { if(d[top[x]]<d[top[y]]) swap(x,y); cnt=(cnt+q(1,id[top[x]],id[x]))%p; x=fa[top[x]]; } if(d[x]>d[y]) swap(x,y); cnt=(cnt+q(1,id[x],id[y]))%p; return cnt; } void us(int x,ll k)//原树节点的子树修改 { u(1,id[x],id[x]+si[x]-1,k); } ll qs(int x)//原树节点的子树点权和查询 { return q(1,id[x],id[x]+si[x]-1)%p; } int main() { scanf("%d%d%d%d",&n,&m,&root,&p); for(int i=1;i<=n;i++) scanf("%d",&a[i]); for(int i=1;i<n;i++) { int x,y; scanf("%d%d",&x,&y); add(x,y),add(y,x); } dfs1(root,0); dfs2(root,root); bu(1,1,n); while(m--) { int op,x,y,z; scanf("%d",&op); if(op==1) { scanf("%d%d%d",&x,&y,&z); up(x,y,z); } if(op==2) { scanf("%d%d",&x,&y); printf("%lld\n",qu(x,y)); } if(op==3) { scanf("%d%d",&x,&z); us(x,z); } if(op==4) { scanf("%d",&x); printf("%lld\n",qs(x)); } } }