树链剖分学习笔记
这篇文章是较于模板的知识,如果想要做题,左转例题篇
定义
树链剖分是什么?
树链剖分指一种对树进行划分的算法,它先通过轻重边剖分将树分为多条链,保证每个点属于且只属于一条链,然后再通过数据结构来维护每一条链,包含重链剖分、长链剖分和实链剖分,我们平常使用的都是重链剖分。
树链剖分解决了什么?
树上的单点(单边)修改,路径修改,子树修改和换根
树上的单点(单边)查询,路径查询,子树查询
是比DFS序和树上差分更多变,比LCT更简单的数据结构。
如何实现树剖
例题引入,已知一棵包含N个结点的树(连通且无环),每个节点上包含一个数值,需要支持以下操作:
操作1:将树从x到y结点最短路径上所有节点的值都加上z
操作2:求树从x到y结点最短路径上所有节点的值之和
操作3:将以x为根节点的子树内所有节点值都加上z
操作4:求以x为根节点的子树内所有节点值之和
子树的操作还好处理,路径上的便有些棘手。思考:为何不能将一条路径操作分段转到序列上,再用序列上的数据结构维护呢?
定义size[x]为以x为根的子树大小,son[x]为x的重儿子(定义为x的儿子中size最大的儿子),对于每个节点:连向它重儿子的边为重边,其余则为轻边。
重边组成的路径叫做重链。
图中黑色的边是重边,打上红点的节点是其父亲的轻儿子(除重儿子以外的儿子)
可以直观的感受到,轻边总是连接着两条重链,而每个点只归属一条重链。 我们萌生出了一个想法,就是存下每一条重链,并在路径操作时分成若干条重链进行操作,即实现了树链剖分。
完善定义
dep[x]:节点x的深度
prt[x]:节点x的父亲
son[x]:节点x的重儿子
size[x]:以x为根的子树大小
top[x]:节点x所在重链的链头
tid[x]:第二次DFS的DFS序
rk[x]:tid的反映射
前面四个数组都可以由第一次DFS求得,在知道重儿子之后,我们就可以第二次DFS求出后三个数组。 第二次DFS的规则是,先搜当前节点的重儿子,再搜轻儿子。
tid有什么用呢?我们把上图的tid写出来: 1 4 9 13 14 8 10 2 6 11 12 5 3 7
首先它是一个DFS序,所以自然满足对于任意一个x,区间[tid[x],tid[x]+size[x]-1]是x的子树,故可以迅速的实现子树操作。
其次,应为DFS的顺序是默认的重儿子优先,所以每一条重链在tid上都是连续的。
说白了,就可以把一条路径拆为若干个在tid上的连续区间,再用数据结构维护。 怎么拆分呢?
类似于LCA的爬树法,选一个深度小的向上跳,对应的区间就是[tid[top[x]],tid[x]],然后通过top[x]的父亲爬到另一条重链上,直到跳到同一条重链上
e.g:拆分(5,14)这条路径:(5,5)+(2,2)+(1,14)
思路大概有了,看一看代码实现。
//预处理代码
void DFS1(int x,int fa,int depth){ dep[x]=depth,prt[x]=fa,size[x]=1; for(int i=h[x];i;i=w[i].nxt){ int v=w[i].to; if(v==fa)continue; DFS1(v,x,depth+1); size[x]+=size[v]; if(size[v]>size[son[x]])son[x]=v;//更新重儿子 } } void DFS2(int x,int sp){ top[x]=sp,tid[x]=++tot,rk[tot]=x; if(!son[x])return; DFS2(son[x],sp);//先搜重儿子 for(int i=h[x];i;i=w[i].nxt){ int v=w[i].to; if(v==prt[x]||v==son[x])continue; DFS2(v,v);//自己为链头搜下去 } }
//对应4个操作的代码
inline void change(int x,int y,int d){ while(top[x]!=top[y]){ if(dep[top[x]]<dep[top[y]])swap(x,y); update(1,tid[top[x]],tid[x],d); x=prt[top[x]]; } if(dep[x]>dep[y])swap(x,y); update(1,tid[x],tid[y],d); } inline int ask(int x,int y){ int ans=0; while(top[x]!=top[y]){ if(dep[top[x]]<dep[top[y]])swap(x,y); ans+=query(1,tid[top[x]],tid[x]); x=prt[top[x]]; } if(dep[x]>dep[y])swap(x,y); return ans+query(1,tid[x],tid[y]); } inline void changeson(int x,int d){ update(1,tid[x],tid[x]+size[x]-1,d); } inline int askson(int x){ return query(1,tid[x],tid[x]+size[x]-1); }
加上线段树,我们愉快地解决了这道题
#include<iostream> #include<iomanip> #include<cstring> #include<cstdio> #include<cmath> #include<algorithm> using namespace std; #define int long long #define INF 0x7fffffff inline int read(){ char ch; int bj=0; while(!isdigit(ch=getchar())) bj|=(ch=='-'); int res=ch^(3<<4); while(isdigit(ch=getchar())) res=(res<<1)+(res<<3)+(ch^(3<<4)); return bj?-res:res; } void printnum(int x){ if(x>9)printnum(x/10); putchar(x%10+'0'); } inline void print(int x,char ch){ if(x<0){ putchar('-'); x=-x; } printnum(x); putchar(ch); } const int MAXN=100005; int n,m,mod,root,cnt,tot; int h[MAXN],size[MAXN],prt[MAXN],son[MAXN],dep[MAXN],tid[MAXN],top[MAXN],rk[MAXN],a[MAXN]; struct Edge { int to,nxt; }w[MAXN<<1]; inline void AddEdge(int x,int y){ w[++cnt].to=y; w[cnt].nxt=h[x]; h[x]=cnt; } void DFS1(int x,int fa,int depth){ dep[x]=depth,prt[x]=fa,size[x]=1; for(int i=h[x];i;i=w[i].nxt){ int v=w[i].to; if(v==fa)continue; DFS1(v,x,depth+1); size[x]+=size[v]; if(size[v]>size[son[x]])son[x]=v; } } void DFS2(int x,int sp){ top[x]=sp,tid[x]=++tot,rk[tot]=x; if(!son[x])return; DFS2(son[x],sp); for(int i=h[x];i;i=w[i].nxt){ int v=w[i].to; if(v==prt[x]||v==son[x])continue; DFS2(v,v); } } struct Segtree { int l,r,sum,bj; }tree[MAXN<<2]; inline void pushup(int k){ tree[k].sum=(tree[k<<1].sum+tree[k<<1|1].sum)%mod; } inline void pushdown(int k){ if(tree[k].bj){ tree[k<<1].bj=(tree[k<<1].bj+tree[k].bj)%mod; tree[k<<1|1].bj=(tree[k<<1|1].bj+tree[k].bj)%mod; tree[k<<1].sum=(tree[k<<1].sum+(tree[k<<1].r-tree[k<<1].l+1)*tree[k].bj%mod)%mod; tree[k<<1|1].sum=(tree[k<<1|1].sum+(tree[k<<1|1].r-tree[k<<1|1].l+1)*tree[k].bj%mod)%mod; tree[k].bj=0; } } inline void build(int k,int l,int r){ tree[k].l=l,tree[k].r=r; if(l==r){ tree[k].sum=a[rk[l]]; return; } int mid=(l+r)>>1; build(k<<1,l,mid); build(k<<1|1,mid+1,r); pushup(k); } inline void update(int k,int l,int r,int d){ if(l>tree[k].r||r<tree[k].l)return; if(l<=tree[k].l&&r>=tree[k].r){ tree[k].bj=(tree[k].bj+d)%mod; tree[k].sum=(tree[k].sum+(tree[k].r-tree[k].l+1)*d%mod)%mod; return; } pushdown(k); update(k<<1,l,r,d); update(k<<1|1,l,r,d); pushup(k); } inline int query(int k,int l,int r){ if(l>tree[k].r||r<tree[k].l)return 0; if(l<=tree[k].l&&r>=tree[k].r)return tree[k].sum; pushdown(k); return (query(k<<1,l,r)+query(k<<1|1,l,r))%mod; } inline void change(int x,int y,int d){ while(top[x]!=top[y]){ if(dep[top[x]]<dep[top[y]])swap(x,y); update(1,tid[top[x]],tid[x],d); x=prt[top[x]]; } if(dep[x]>dep[y])swap(x,y); update(1,tid[x],tid[y],d); } inline int ask(int x,int y){ int ans=0; while(top[x]!=top[y]){ if(dep[top[x]]<dep[top[y]])swap(x,y); ans=(ans+query(1,tid[top[x]],tid[x]))%mod; x=prt[top[x]]; } if(dep[x]>dep[y])swap(x,y); return (ans+query(1,tid[x],tid[y]))%mod; } inline void changeson(int x,int d){ update(1,tid[x],tid[x]+size[x]-1,d); } inline int askson(int x){ return query(1,tid[x],tid[x]+size[x]-1); } signed main(){ n=read(),m=read(),root=read(),mod=read(); for(int i=1;i<=n;i++)a[i]=read(); int op,x,y,d; for(int i=1;i<n;i++){ x=read(),y=read(); AddEdge(x,y),AddEdge(y,x); } DFS1(root,0,1); DFS2(root,root); build(1,1,n); while(m--){ op=read(); if(op==1){ x=read(),y=read(),d=read(); change(x,y,d); } else if(op==2){ x=read(),y=read(); print(ask(x,y),'\n'); } else if(op==3){ x=read(),d=read(); changeson(x,d); } else { x=read(); print(askson(x),'\n'); } } return 0; }
树链剖分时间复杂度
引理:若(u,v)是轻边,则size[v] < size[u]/2
证明:显然。
有此可得:每跳一次轻边,子树大小就除以2,故拆路径的时间复杂度是O(log n) 这就是树剖为什么按照轻重划分的原因。
树剖求LCA
既然树剖也是爬树,那为何不能求LCA呢?想一想,当x和y跳到同一重链时,深度小的那个不就是LCA吗? 时间复杂度:O(log n)。
inline int LCA(int x,int y){ while(top[x]!=top[y]){ if(dep[top[x]]<dep[top[y]])swap(x,y); x=prt[top[x]]; } if(dep[x]>dep[y])swap(x,y); return x; }
偷偷说一句:树剖实际仅次于Tarjan求LCA,有时把欧拉环游序吊起来打(逃。
因为极端数据卡不住树剖,在满二叉树时O(log n)才会跑满。 在空间卡得紧的情况下,树剖是求LCA的最佳选择