浅谈树链剖分—轻重链剖分
闲话
似乎会有很多种树剖,什么长链剖分之类的,但是暂时只会轻重链剖分(可怜)。
以前的版本在这里,但是感觉写的太粗糙了,所以决定重写一篇(我也不知道为什么要一直写树剖而不写点别的)。
正文
引入
一些树上问题并不是很好解决,因为并不是对连续的区间进行处理。所以,今天的主角——轻重链剖分,应运而生。有了它,我们就可以把树变成一条条链进行处理,非常厉害,简直是黑科技(捧读)。
algorithm
定义
在介绍具体的算法流程前,我们需要明确一些树上的定义:
- 重儿子:对于每一个非叶子节点,它的子节点中,子树大小最大的那个子节点被称为重儿子。
- 轻儿子:对于每一个非叶子节点,除了重儿子以外的子节点都是它的轻儿子。
- 重边:任意两个重儿子或重儿子和其父节点之间的边称为重边。
- 轻边:除去重边剩下的边。
- 重链:重边相连组成的链即为重链,且重链一定以轻儿子为起点。
- 此外,若一个叶子节点是轻儿子,那么它也可以视为一条重链。
这些就比较形式化了。具体一些:
\(1\) 号点的重儿子是 \(4\),因为 \(2,4,7\) 中 \(4\) 的子树大小最大,以此类推得到所有的重儿子,即可划分出重边与重链。
图中黄色的边即为重边,粉色的边为轻边。那么 \(1-4-5-8-9\) 即为一条重链。
\(\color{#FF69B4}\texttt{明确了以上的定义,就可以正式开始剖了!(撒花✿✿✿)}\)
核心思路
-
我们的目的是把树变成便于处理的连续区间,要怎么做呢?
- 似乎可以利用 dfs 序,但是正常 dfs 的时候每一次选择进入的子节点是随机的(其实是按照输入的顺序),并不能很好地解决问题。
-
这时,就可以利用上文提到的重儿子与重链了。
- 我们可以在 dfs 时选择优先选择重儿子,并且记录每一个点所在的重链的链顶,这样就把一棵树剖成了许多的重链,并且每条重链上的 dfs 序是连续的。
- 重链的维护可以使用数据结构,每两条重链之间的轻边就直接处理。
-
具体到代码中,我们需要进行两次 dfs ,第一次要求出重儿子,顺带求出每个点的父节点、深度和子树大小。
-
第二次按照先重儿子再轻儿子的顺序进行 dfs,记录每个点的 dfs 序和所在链顶。
- 如果要用到线段树等数据结构维护,还需要记录当前点的 dfs 序所对应的点的权值(这地方文字写出来很绕,可以看代码 \(18\) 行),记录编号也可以,可以理解为将原来的权值数组(假如是 \(a\))按照 dfs 序重新排列得到 \(a'\)。因为数据结构要维护的是 dfs 序对应的序列,也就是 \(a'\),而不是 \(a\)。
-
时间复杂度 \(O(n+m)\)。
code
void dfs1(int u,int f){ int max_son=-1;//当前重儿子的子树大小 dep[u]=dep[f]+1;fa[u]=f; sz[u]=1;//sz:子树大小 for(int i=head[u];i;i=edge[i].nex){ int v=edge[i].to; if(v==f) continue; dfs1(v,u); sz[u]+=sz[v]; if(sz[v]>max_son){ son[u]=v; max_son=sz[v]; }//找出重儿子 } } void dfs2(int u,int topx){//topx为当前节点所在链顶 dfn[u]=++cnt;//记录dfs序 pos[cnt]=a[u];//dfs序为cnt的点的值为a[u] top[u]=topx;//链顶 if(!son[u]) return ; dfs2(son[u],topx);//优先处理重儿子 for(int i=head[u];i;i=edge[i].nex){//处理剩下的轻儿子 int v=edge[i].to; if(v==fa[u]||v==son[u]) continue; dfs2(v,v); } }
简单运用
其实你可以认为上面是整个轻重链剖分的全部,因为它确实剖分了。不过也可以把上面当成一种预处理,即处理出 \(fa,dep,sz,top,pos,dfn\) 等数组,方便后续的处理。
洛谷 P3384 【模板】重链剖分/树链剖分
- 两次 dfs 后,利用线段树维护重链上的信息即可。
- 在前两个操作中,每一次选择 \(x,y\) 链顶深度较深的那个点(假设为 \(x\)),在线段树上维护 \([dfn[top[x]],dfn[x]]\) 这个区间(也就是 \(x\) 所在的重链),再将 \(x\) 跳到下一条链的链底 \(fa[top[x]]\),重复这个步骤。直到 \(x,y\) 都在一条链上了,就维护 \(x,y\) 之间的部分。
- 后两个操作更为简单,因为子树里的 dfs 序是连续的,直接维护就好了。
code
void add_1(int x,int y,ll k){ k%=p; while(top[x]!=top[y]){ if(dep[top[x]]<dep[top[y]]) swap(x,y); modify(1,1,n,dfn[top[x]],dfn[x],k); x=fa[top[x]]; } if(dep[x]>dep[y]) swap(x,y); modify(1,1,n,dfn[x],dfn[y],k); } ll query_2(int x,int y){ ll res=0; while(top[x]!=top[y]){ if(dep[top[x]]<dep[top[y]]) swap(x,y); res+=query(1,1,n,dfn[top[x]],dfn[x]); x=fa[top[x]]; } if(dep[x]>dep[y]) swap(x,y); res+=query(1,1,n,dfn[x],dfn[y]); return res%p; }//代码很重复有没有 void add_3(int x,ll k){ k%=p; modify(1,1,n,dfn[x],dfn[x]+sz[x]-1,k); } ll query_4(int x){ return query(1,1,n,dfn[x],dfn[x]+sz[x]-1); }//不要忘记-1,sz[x]包括x自己
模板就是模板,这种树上修改查询的题感觉一共也就这些操作种类了,题目间的差别主要在于线段树维护的信息不同。
求解 \(\texttt{LCA}\)
目录上求解跑到LCA后面去了,怪事
- 求 \(\texttt{LCA}\) 的过程实际上跟模板题里面 \(1,2\) 操作很像。每一次选择 \(x,y\) 链顶深度较深的那个点(假设为 \(x\)),将 \(x\) 跳到下一条链的链底 \(fa[top[x]]\),重复这个步骤。直到 \(x,y\) 都在一条链上了,\(\texttt{LCA}\) 就是 \(x,y\) 中深度更浅的那一个。
- 时间复杂度 \(O(n+m\log n)\)(\(O(n)\) 是预处理,查询 \(m\) 次,单次 \(O(\log n)\))。
code
int LCA(int x,int y){ while(top[x]!=top[y]){ if(dep[top[x]]<dep[top[y]]) swap(x,y); x=fa[top[x]]; } return dep[x]>dep[y]?y:x; }
现在你可以抛弃倍增了,毕竟树剖常数小,还可以求出其他信息比如子树大小,万一有用呢
- 关于单次查询的时间复杂度:一个点到根节点的轻边数是 \(\log n\) 级别的,因为如果一个点是轻儿子,它的父节点所有儿子里,一定会有一个重儿子的 \(sz\geq\) 它的 \(sz\),所以跳一次轻边就至少将当前子树中节点数\(\times 2\)。故最多跳 \(\log n\) 次,也就是循环 \(\log n\) 次,所以单次查找为 \(O(\log n)\)。
一些练习题
板子
[ZJOI2008] 树的统计
[NOI2015] 软件包管理器
[HAOI2015] 树上操作
[SHOI2012] 魔法树
问:CCF什么时候再出一次模板题?
洛谷 P4114 Qtree1
单点修改,区间最大值,但是给出的是边权。显然边权是没法直接维护的,我们需要点权。而除了根节点,每个节点又正好对应一条边,那么就可以把每条边的权值赋给深度较深的那个端点,根节点赋为 \(0\),查询时忽略 \(\texttt{LCA}\) 即可。
有可能出现 \(x\) 和 \(y\) 最终跳到一个点的情况,而我们要忽略 \(\texttt{LCA}\),修改的是 \([dfn[x]+1,dfn[y]]\),会出现左端点大于右端点的情况。可以特判排除这种情况,但其实线段树上并不能找到这样的区间,所以不会对最终答案造成影响。
code
void dfs1(int u,int f){//边权转点权 sz[u]=1; fa[u]=f; dep[u]=dep[f]+1; int max_son=-1; for(int i=head[u];i;i=edge[i].nex){ int v=edge[i].to,id=edge[i].id; if(v==f) continue; mapi[id]=v;//记录每条边对应的节点 val[v]=edge[i].w; dfs1(v,u); sz[u]+=sz[v]; if(sz[v]>max_son){ son[u]=v; max_son=sz[v]; } } } //主函数中 if(s[0]=='Q'){ while(top[u]!=top[v]) { if(dep[top[u]]<dep[top[v]]) swap(u,v); res=max(res,Max(1,1,n,dfn[top[u]],dfn[u])); u=fa[top[u]]; } if(dep[u]>dep[v]) swap(u,v); res=max(res,Max(1,1,n,dfn[u]+1,dfn[v]));//加1为了排除LCA }
其余代码都跟模板类似,就不贴了。
洛谷 P4315 月下“毛景树”
- 这道题可以说是上一题的升级版,同样是边权转点权,还涉及了单点修改,区间加,区间覆盖以及查询区间最值,细节问题很多。
- 边权转点权的处理还是同样,赋值给深度较深的点。
- 对于线段树的部分,我们有两个标记,一个是区间加的,记为 \(add\),一个是区间覆盖的,记为 \(cov\)。一定要注意这两个的顺序。每一次区间加,正常处理 \(add\) 即可;区间覆盖时,要把当前的 \(add\) 清空。这样可以保证当前的 \(cov\) 一定是在 \(add\) 之前操作的,故 \(\operatorname{pushdown}\) 的时候要先处理 \(cov\) 再处理 \(add\)。
- 别忘记跳重链时最后要忽略 \(\texttt{LCA}\)。
code
点击查看代码
#include<iostream> #include<cstdio> #include<algorithm> #include<cmath> #include<cstring> #include<string> #include<utility> #include<vector> #include<queue> #include<bitset> #include<map> #define int long long #define FOR(i,a,b) for(register int i=a;i<=b;i++) #define ROF(i,a,b) for(register int i=a;i>=b;i--) #define mp(a,b) make_pair(a,b) #define pll pair<long long,long long> #define pii pair<int,int> #define fi first #define se second using namespace std; inline int read(); typedef long long ll; const int N=1e5+5; const int INF=0x3f3f3f3f; int n,m,k; struct E{ int to,nex,w,id; }edge[N<<1]; int head[N],cnt=0; void add(int u,int v,int w,int id){ edge[++cnt]=(E){v,head[u],w,id}; head[u]=cnt; } int mapi[N],pos[N],dfn[N],top[N],sz[N],son[N],fa[N],dep[N],val[N]; void dfs1(int u,int f){ sz[u]=1; fa[u]=f; dep[u]=dep[f]+1; int max_son=-1; for(int i=head[u];i;i=edge[i].nex){ int v=edge[i].to; if(v==f) continue; mapi[edge[i].id]=v; val[v]=edge[i].w; dfs1(v,u); sz[u]+=sz[v]; if(sz[v]>max_son){ son[u]=v; max_son=sz[v]; } } } int Cnt=0; void dfs2(int u,int topx){ dfn[u]=++Cnt; pos[Cnt]=val[u]; top[u]=topx; if(!son[u]) return ; dfs2(son[u],topx); for(int i=head[u];i;i=edge[i].nex){ int v=edge[i].to; if(v==fa[u]||v==son[u]) continue; dfs2(v,v); } } int lazy[N<<2],maxn[N<<2],cov[N<<2]; void pushup(int u){ maxn[u]=max(maxn[u<<1],maxn[u<<1|1]); } void build(int u,int l,int r){ cov[u]=-INF; if(l==r){ maxn[u]=pos[l]; return ; } int mid=l+r>>1; build(u<<1,l,mid); build(u<<1|1,mid+1,r); pushup(u); } void pushdown(int u,int l,int r){ int mid=l+r>>1; int ls=u<<1,rs=u<<1|1,Ll=mid-l+1,Lr=r-mid; if(cov[u]!=-INF){ cov[ls]=cov[rs]=cov[u]; lazy[ls]=lazy[rs]=0; maxn[ls]=maxn[rs]=cov[u]; cov[u]=-INF; } if(lazy[u]){ lazy[ls]+=lazy[u]; lazy[rs]+=lazy[u]; maxn[ls]+=lazy[u]; maxn[rs]+=lazy[u]; lazy[u]=0; } } void change(int u,int l,int r,int x,int k){ if(l==r&&l==x){ maxn[u]=k; return ; } pushdown(u,l,r); int mid=l+r>>1; if(x<=mid) change(u<<1,l,mid,x,k); else change(u<<1|1,mid+1,r,x,k); pushup(u); } void cover(int u,int l,int r,int L,int R,int k){ if(L<=l&&r<=R){ maxn[u]=k; cov[u]=k; lazy[u]=0; return ; } pushdown(u,l,r); int mid=l+r>>1; if(L<=mid) cover(u<<1,l,mid,L,R,k); if(R>mid) cover(u<<1|1,mid+1,r,L,R,k); pushup(u); } void Add(int u,int l,int r,int L,int R,int k){ if(L<=l&&r<=R){ maxn[u]+=k; lazy[u]+=k; return ; } pushdown(u,l,r); int mid=l+r>>1; if(L<=mid) Add(u<<1,l,mid,L,R,k); if(R>mid) Add(u<<1|1,mid+1,r,L,R,k); pushup(u); } int Max(int u,int l,int r,int L,int R){ if(L<=l&&r<=R){ return maxn[u]; } pushdown(u,l,r); int mid=l+r>>1; int res=0; if(L<=mid) res=max(Max(u<<1,l,mid,L,R),res); if(R>mid) res=max(Max(u<<1|1,mid+1,r,L,R),res); return res; } signed main() { n=read(); FOR(i,1,n-1){ int u=read(),v=read(),w=read(); add(u,v,w,i); add(v,u,w,i); } dfs1(1,0); dfs2(1,1); build(1,1,Cnt); char s[10]; while(~scanf("%s",s)){ if(s[0]=='S') break; if(s[0]=='M'){ int u=read(),v=read(); int res=0; while(top[u]!=top[v]){ if(dep[top[u]]<dep[top[v]]) swap(u,v); res=max(res,Max(1,1,n,dfn[top[u]],dfn[u])); u=fa[top[u]]; } if(dep[u]>dep[v]) swap(u,v); res=max(res,Max(1,1,n,dfn[u]+1,dfn[v])); printf("%lld\n",res); } else if(s[0]=='A'){ int u=read(),v=read(); k=read(); while(top[u]!=top[v]){ if(dep[top[u]]<dep[top[v]]) swap(u,v); Add(1,1,n,dfn[top[u]],dfn[u],k); u=fa[top[u]]; } if(dep[u]>dep[v]) swap(u,v); if(u!=v) Add(1,1,n,dfn[u]+1,dfn[v],k); } else if(s[1]=='h'){ k=read(); int z=read(); change(1,1,n,dfn[mapi[k]],z); } else{ int u=read(),v=read(); k=read(); while(top[u]!=top[v]){ if(dep[top[u]]<dep[top[v]]) swap(u,v); cover(1,1,n,dfn[top[u]],dfn[u],k); u=fa[top[u]]; } if(dep[u]>dep[v]) swap(u,v); if(u!=v) cover(1,1,n,dfn[u]+1,dfn[v],k); } } return 0; } inline int read() { int x=0,f=1;char ch=getchar(); while(ch<'0'||ch>'9') {if(ch=='-') f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();} return f*x; }
洛谷 P1505 [国家集训队] 旅游
- 这道题更加麻烦,还是先边权转点权。之后需要建线段树,单点修改、区间取反、区间和、区间最大值、最小值。
- 取反的操作和单点修改并不冲突,也没有区间修改,只有一个取反的标记,所以直接下传就好了。下传时区间和,最大值和最小值都直接取反,再把最大值和最小值交换一下。每次取反将标记异或 \(1\)。
code
点击查看代码
#include<iostream> #include<cstdio> #include<algorithm> #include<cmath> #include<cstring> #include<string> #include<utility> #include<vector> #include<queue> #include<bitset> #include<map> #define FOR(i,a,b) for(register int i=a;i<=b;i++) #define ROF(i,a,b) for(register int i=a;i>=b;i--) #define mp(a,b) make_pair(a,b) #define pll pair<long long,long long> #define pii pair<int,int> #define fi first #define se second using namespace std; inline int read(); typedef long long ll; const int N=2e5+5; const int INF=0x3f3f3f3f; int n,m,k; struct E{ int to,nex,w,id; }edge[N<<1]; int head[N],cnt=0; void Add(int u,int v,int w,int id){ edge[++cnt]=(E){v,head[u],w,id}; head[u]=cnt; } int fa[N],son[N],dfn[N],pos[N],val[N],mapi[N],top[N],dep[N],sz[N]; int tr[N<<2],maxn[N<<2],minn[N<<2],rev[N<<2]; void dfs1(int u,int f){ sz[u]=1; dep[u]=dep[f]+1; fa[u]=f; int max_son=-1; for(int i=head[u];i;i=edge[i].nex){ int v=edge[i].to,w=edge[i].w,id=edge[i].id; if(v==f) continue; mapi[id]=v; val[v]=w; dfs1(v,u); sz[u]+=sz[v]; if(sz[v]>max_son){ max_son=sz[v]; son[u]=v; } } } int tot=0; void dfs2(int u,int topx){ dfn[u]=++tot; pos[tot]=val[u]; top[u]=topx; if(!son[u]) return ; dfs2(son[u],topx); for(int i=head[u];i;i=edge[i].nex){ int v=edge[i].to; if(v==fa[u]||v==son[u]) continue; dfs2(v,v); } } #define ls(u) u<<1 #define rs(u) u<<1|1 void pushup(int u){ tr[u]=tr[ls(u)]+tr[rs(u)]; maxn[u]=max(maxn[ls(u)],maxn[rs(u)]); minn[u]=min(minn[ls(u)],minn[rs(u)]); } void build(int u,int l,int r){ if(l==r){ tr[u]=maxn[u]=minn[u]=pos[l]; return ; } int mid=l+r>>1; build(u<<1,l,mid); build(u<<1|1,mid+1,r); pushup(u); } void pushdown(int u,int l,int r){ if(rev[u]){ rev[u]=0; rev[ls(u)]^=1; rev[rs(u)]^=1; tr[ls(u)]=-tr[ls(u)]; tr[rs(u)]=-tr[rs(u)]; maxn[ls(u)]=-maxn[ls(u)]; minn[ls(u)]=-minn[ls(u)]; swap(maxn[ls(u)],minn[ls(u)]); maxn[rs(u)]=-maxn[rs(u)]; minn[rs(u)]=-minn[rs(u)]; swap(maxn[rs(u)],minn[rs(u)]); } } void modify(int u,int l,int r,int x,int k){ if(l==r&&l==x){ tr[u]=maxn[u]=minn[u]=k; return ; } pushdown(u,l,r); int mid=l+r>>1; if(x<=mid) modify(ls(u),l,mid,x,k); else modify(rs(u),mid+1,r,x,k); pushup(u); } void Re(int u,int l,int r,int L,int R){ if(L<=l&&r<=R){ rev[u]^=1; tr[u]=-tr[u]; maxn[u]=-maxn[u]; minn[u]=-minn[u]; swap(maxn[u],minn[u]); return ; } pushdown(u,l,r); int mid=l+r>>1; if(L<=mid) Re(ls(u),l,mid,L,R); if(R>mid) Re(rs(u),mid+1,r,L,R); pushup(u); } int qsum(int u,int l,int r,int L,int R){ int res=0; if(L<=l&&r<=R){ return tr[u]; } pushdown(u,l,r); int mid=l+r>>1; if(L<=mid) res+=qsum(ls(u),l,mid,L,R); if(R>mid) res+=qsum(rs(u),mid+1,r,L,R); return res; } int qmax(int u,int l,int r,int L,int R){ int res=-2000; if(L<=l&&r<=R){ return maxn[u]; } pushdown(u,l,r); int mid=l+r>>1; if(L<=mid) res=max(res,qmax(ls(u),l,mid,L,R)); if(R>mid) res=max(res,qmax(rs(u),mid+1,r,L,R)); return res; } int qmin(int u,int l,int r,int L,int R){ int res=2000; if(L<=l&&r<=R){ return minn[u]; } pushdown(u,l,r); int mid=l+r>>1; if(L<=mid) res=min(res,qmin(ls(u),l,mid,L,R)); if(R>mid) res=min(res,qmin(rs(u),mid+1,r,L,R)); return res; } int main() { n=read(); FOR(i,1,n-1){ int u=read()+1,v=read()+1,w=read(); Add(u,v,w,i),Add(v,u,w,i); } m=read(); dfs1(1,0); dfs2(1,1); build(1,1,n); while(m--){ char s[5]; scanf("%s",s); int u=read()+1,v=read()+1; if(s[0]=='C'){ modify(1,1,n,dfn[mapi[u-1]],v-1); } else if(s[0]=='N'){ while(top[u]!=top[v]){ if(dep[top[u]]<dep[top[v]]) swap(u,v); Re(1,1,n,dfn[top[u]],dfn[u]); u=fa[top[u]]; } if(dep[u]>dep[v]) swap(u,v); Re(1,1,n,dfn[u]+1,dfn[v]); } else if(s[0]=='S'){ int res=0; while(top[u]!=top[v]){ if(dep[top[u]]<dep[top[v]]) swap(u,v); res+=qsum(1,1,n,dfn[top[u]],dfn[u]); u=fa[top[u]]; } if(dep[u]>dep[v]) swap(u,v); res+=qsum(1,1,n,dfn[u]+1,dfn[v]); printf("%d\n",res); } else if(s[1]=='A'){ int res=-2000; while(top[u]!=top[v]){ if(dep[top[u]]<dep[top[v]]) swap(u,v); res=max(res,qmax(1,1,n,dfn[top[u]],dfn[u])); u=fa[top[u]]; } if(dep[u]>dep[v]) swap(u,v); res=max(res,qmax(1,1,n,dfn[u]+1,dfn[v])); printf("%d\n",res); } else{ int res=2000; while(top[u]!=top[v]){ if(dep[top[u]]<dep[top[v]]) swap(u,v); res=min(res,qmin(1,1,n,dfn[top[u]],dfn[u])); u=fa[top[u]]; } if(dep[u]>dep[v]) swap(u,v); res=min(res,qmin(1,1,n,dfn[u]+1,dfn[v])); printf("%d\n",res); } } return 0; } inline int read() { int x=0,f=1;char ch=getchar(); while(ch<'0'||ch>'9') {if(ch=='-') f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();} return f*x; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步