【算法学习】【洛谷】树链剖分 & P3384 【模板】树链剖分 P2146 软件包管理器

时间:2023-03-09 15:37:41
【算法学习】【洛谷】树链剖分 & P3384 【模板】树链剖分 P2146 软件包管理器

刚学的好玩算法,AC2题,非常开心。

其实很早就有教过,以前以为很难就没有学,现在发现其实很简单也很有用。

更重要的是我很好调试,两题都是几乎一遍过的。


介绍树链剖分前,先确保已经学会以下基本技巧:

DFS序列,线段树/树状数组,LCA(最近公共祖先)

DFS序列确保你能听懂以下环节,线段树/树状数组是维护序列的有力工具,而LCA涉及树上的很多基本问题。


经常会遇到这样的题目:

对于一棵树,给x到y的路径上的点/边都做一个操作,并且查询x到y的路径上的点/边的值。

如果不是x到y的路径,而是节点x的子树,这可以方便地用DFS序列维护。

因为一棵子树在一个DFS序列中是连续的一段区间。

用类似的思想,我们尝试把树上的链也变成一段连续区间。

然而这是不可能的,因为链上是有分支的。

于是我们退而求其次,把一个链分成有限个区间,这边是有可能实现的,应运而生的算法就是树链剖分。


对于一棵树上的每个节点\(x\),我们定义\(siz(x)\)为以\(x\)为根的子树的节点个数。

对于一棵树上的每一个非叶子节点,我们找到它的儿子们中\(siz\)最大的那一个,并且标记它到这个儿子的这条边。

那么对树上所有点都进行这个操作之后,被标记的边会形成若干条链,并且覆盖每一个节点。

我们把这样的边叫做重边,其他的边叫做轻边,把连续重边形成的链叫做重链,一个点也可以组成一条长度为1的重链。


定理①:每个点属于且只属于一个重链。

定理②:从一个点到根的路径中,重链个数不超过\(O(log_2n)\)个,\(n\)表示树大小。

证明:从一个点向它的父亲走,如果这条边是轻边,那么子树的节点数至少翻倍,重链个数加一。

   那么显然轻边个数不超过\(O(log_2n)\),而不是轻边的就组成重链,于是重链个数也不超过\(O(log_2n)\)个。


好了,我们知道了重链的定义,性质,如何使用它呢?

因为一个点到根不超过\(O(log_2n)\)条重链,那么任意两点间也不会超过\(O(log_2n)\)条重链。

而每条重链都是一段连续的区间,那么链上问题就变成了区间问题的并了。

这就实现了链到区间的转化。


要想使用树链剖分,需要在代码中实现它,我们需要做些什么?

先考虑用使用重链,求出任意两点之间的路径吧,或者,求出它们的LCA。

考虑初始时\(x,y\)两点,我们发现每一条重链都能直接用它的最顶端的节点表示,记作\(top\)节点。

那么先看\(x,y\)属不属于同一条重链:\(top(x)=top(y)\)?

只要不属于,一定要有一个跑出它的重链,实际上,必然是\(top(x),top(y)\)中深度更深的那个跳出其重链。

不妨设其为\(x\),那么\(x\to y\)的路径上就多了一条\(top_x\to x\)的路径,之后执行\(x=faz(top(x))\),\(faz(x)\)表示\(x\)的父亲节点。

不断重复这个操作,直到\(top(x)=top(y)\),那么这时\(x,y\)的LCA就是\(x,y\)中深度小的那个了。

这样的时间复杂度是\(O(log_2n)\)的。


在做这些操作前,我们需要先求出\(top(x)\)、\(siz(x)\)、\(dep(x)\)、\(faz(x)\)等关键数组。

如何求出?其实一次DFS就够了。

DFS(u){
dep[u] = dep[faz[u]] + 1
Heavy = 0, siz[u] = 1
for ( i = u.sons ) {
faz[i] = u
DFS(i)
siz[u] = siz[u] + siz[i]
if ( siz[Heavy] < siz[i] )
Heavy = i
}
son[u] = Heavy
for ( i = u.sons ) {
if ( i != Heavy ) {
j=i
while ( j != NULL )
top[j] = i, j = son[j]
}
}
}

这样就能处理好了,这里的\(son[x]\)表示\(x\)的重儿子。

还有一个小问题就是:我们没有把树中的每个节点映射到序列中的某个编号上。

在通常的DFS序列中这个编号就是\(dfn\)数组,在树链剖分中,我们也可以使用\(dfn\)数组,但是要满足链上的点在一个区间中。

做到这一点也不难,直接在里面一样做就好,也不用第二个DFS。

问题在于,有的题目不止要你改链上的点,还要子树修改,而一棵树的总重链数是可以到\(O(n)\)级别的,自然不能也用链做。

然而子树修改是个很简单的东西,只要满足子树内有序就好了,但是要同时满足的话……

也不是不可能,再写一个DFS就好啦。就和平常的求DFS序列的DFS一样,不过要注意先遍历到重儿子,这样可以保证重链上的点的编号连续。


总结:

树链剖分,DFS序列,本质上都是一样的,相当于一个树到数组的映射,把分支的结构变成连续的结构,并且满足一些性质。

DFS序列只要满足子树的节点是连续的即可,而树链剖分还要满足特定树链的节点连续。

这必然使树链剖分有更强的操作性,树链剖分是很重要的静态在线树上算法,通过预处理,一切操作都可以在线地完成。


接下来放两道例题:

洛谷 P3384 【模板】树链剖分

直接的树链剖分模板,使用线段树维护区间。

// luogu-judger-enable-o2
#include<cstdio>
#define F(i,a,b) for(int i=a;i<=(b);++i)
#define F2(i,a,b) for(int i=a;i<(b);++i)
#define eF(i,u) for(int i=h[u];i;i=nxt[i])
#define swap(a,b) tmp=a, a=b, b=tmp int n,q,R,P,a[100001],b[100001],tmp;
int h[100001],to[200001],nxt[200001],tot;
inline void ins(int x,int y){nxt[++tot]=h[x];to[tot]=y;h[x]=tot;}
int top[100001],son[100001],siz[100001],dep[100001],faz[100001],dfn[100001],cnt;
int dat[262145],laz[262145]; inline void Ex(int u){
for(int i=u;i;i=son[i]) top[i]=u;
} void DFS(int u,int f){
siz[u]=1; dep[u]=dep[faz[u]]+1;
int Heavy=0;
eF(i,u) if(to[i]!=f){
DFS(to[i],faz[to[i]]=u);
siz[u]+=siz[to[i]];
if(siz[Heavy]<siz[to[i]]) Heavy=to[i];
}
son[u]=Heavy;
eF(i,u) if(to[i]!=f) if(to[i]!=Heavy) Ex(to[i]);
} void DFS2(int u,int f){
dfn[u]=++cnt; b[cnt]=a[u];
if(son[u]) DFS2(son[u],u);
eF(i,u) if(to[i]!=f) if(to[i]!=son[u]) DFS2(to[i],u);
} void build(int i,int l,int r){
if(l==r) {dat[i]=b[l]%P; return;}
int mid=l+r>>1;
build(i<<1,l,mid), build(i<<1|1,mid+1,r);
dat[i]=(dat[i<<1]+dat[i<<1|1])%P;
} inline void push_down(int i,int len){
int len2=len>>1;
dat[i<<1]=(dat[i<<1]+(len-len2)*laz[i])%P;
dat[i<<1|1]=(dat[i<<1|1]+len2*laz[i])%P;
laz[i<<1]=(laz[i<<1]+laz[i])%P;
laz[i<<1|1]=(laz[i<<1|1]+laz[i])%P;
laz[i]=0;
} void Add(int i,int l,int r,int a,int b,int x){
if(r<a||b<l) return;
if(a<=l&&r<=b) {dat[i]=(dat[i]+x*(r-l+1))%P; laz[i]=(laz[i]+x)%P; return;}
push_down(i,r-l+1);
int mid=l+r>>1;
Add(i<<1,l,mid,a,b,x); Add(i<<1|1,mid+1,r,a,b,x);
dat[i]=(dat[i<<1]+dat[i<<1|1])%P;
} int Qur(int i,int l,int r,int a,int b){
if(r<a||b<l) return 0;
if(a<=l&&r<=b) return dat[i];
push_down(i,r-l+1);
int mid=l+r>>1;
return (Qur(i<<1,l,mid,a,b)+Qur(i<<1|1,mid+1,r,a,b))%P;
} inline void Add(int x,int y,int z){
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);
Add(1,1,n,dfn[top[x]],dfn[x],z);
x=faz[top[x]];
}
if(dep[x]>dep[y]) swap(x,y);
Add(1,1,n,dfn[x],dfn[y],z);
} inline int Qur(int x,int y){
int ans=0;
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);
ans=(ans+Qur(1,1,n,dfn[top[x]],dfn[x]))%P;
x=faz[top[x]];
}
if(dep[x]>dep[y]) swap(x,y);
return (ans+Qur(1,1,n,dfn[x],dfn[y]))%P;
} int main(){
scanf("%d%d%d%d",&n,&q,&R,&P);
F(i,1,n) scanf("%d",a+i);
int x,y; F2(i,1,n) scanf("%d%d",&x,&y), ins(x,y), ins(y,x);
dep[R]=1; DFS(R,0); Ex(R);
DFS2(R,0);
build(1,1,n);
F(i,1,q){
int opt,x,y,z;
scanf("%d",&opt);
if(opt==1) scanf("%d%d%d",&x,&y,&z), Add(x,y,z);
if(opt==2) scanf("%d%d",&x,&y), printf("%d\n",Qur(x,y));
if(opt==3) scanf("%d%d",&x,&z), Add(1,1,n,dfn[x],dfn[x]+siz[x]-1,z);
if(opt==4) scanf("%d",&x), printf("%d\n",Qur(1,1,n,dfn[x],dfn[x]+siz[x]-1));
}
return 0;
}

洛谷 P2146 软件包管理器

NOI2015的题目,要在树上进行清零和置一的操作。有链上和子树上的操作。

#include<cstdio>
#include<algorithm>
#define F(i,a,b) for(int i=a;i<=(b);++i)
#define F2(i,a,b) for(int i=a;i<(b);++i)
#define eF(i,u) for(int i=h[u];i;i=nxt[i])
using namespace std; int n,q,a[100005],b[100005],tmp;
int h[100005],to[100005],nxt[100005],tot;
inline void ins(int x,int y){nxt[++tot]=h[x];to[tot]=y;h[x]=tot;}
int top[100005],son[100005],siz[100005],dep[100005],faz[100005],dfn[100005],cnt;
int dat[262145],laz[262145]; inline void Ex(int u){
for(int i=u;i;i=son[i]) top[i]=u;
} void DFS(int u){
siz[u]=1; dep[u]=dep[faz[u]]+1;
int Heavy=0;
eF(i,u){
DFS(to[i]);
siz[u]+=siz[to[i]];
if(siz[Heavy]<siz[to[i]]) Heavy=to[i];
}
son[u]=Heavy;
eF(i,u) if(to[i]!=Heavy) Ex(to[i]);
} void DFS2(int u){
dfn[u]=++cnt; b[cnt]=a[u];
if(son[u]) DFS2(son[u]);
eF(i,u) if(to[i]!=son[u]) DFS2(to[i]);
} inline void push_down(int i,int len){
if(!laz[i]) return;
int len2=len>>1;
laz[i<<1]=laz[i<<1|1]=laz[i];
if(laz[i]==1) dat[i<<1]=dat[i<<1|1]=0;
else dat[i<<1]=len-len2, dat[i<<1|1]=len2;
laz[i]=0;
} void Mdf(int i,int l,int r,int a,int b,int x){
if(r<a||b<l) return;
if(a<=l&&r<=b) {dat[i]=x?r-l+1:0; laz[i]=x+1; return;}
push_down(i,r-l+1);
int mid=l+r>>1;
Mdf(i<<1,l,mid,a,b,x); Mdf(i<<1|1,mid+1,r,a,b,x);
dat[i]=dat[i<<1]+dat[i<<1|1];
} int Qur(int i,int l,int r,int a,int b){
if(r<a||b<l) return 0;
if(a<=l&&r<=b) return dat[i];
push_down(i,r-l+1);
int mid=l+r>>1;
return Qur(i<<1,l,mid,a,b)+Qur(i<<1|1,mid+1,r,a,b);
} inline void Mdf(int y){
int x=1;
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);
Mdf(1,1,n,dfn[top[x]],dfn[x],1);
x=faz[top[x]];
}
if(dep[x]>dep[y]) swap(x,y);
Mdf(1,1,n,dfn[x],dfn[y],1);
} inline int Qur(int y){
int x=1,ans=0;
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);
ans=ans+Qur(1,1,n,dfn[top[x]],dfn[x]);
x=faz[top[x]];
}
if(dep[x]>dep[y]) swap(x,y);
return ans+Qur(1,1,n,dfn[x],dfn[y]);
} int main(){
scanf("%d",&n);
int x; F2(i,1,n) scanf("%d",&x), ins(x+1,i+1), faz[i+1]=x+1;
++n;
dep[1]=1; DFS(1); Ex(1);
DFS2(1);
scanf("%d",&q);
F(i,1,q){
char s[10];
int x,y;
scanf("%s%d",s,&x); ++x;
if(s[0]=='i') y=Qur(x), Mdf(x), printf("%d\n",Qur(x)-y);
else printf("%d\n",Qur(1,1,n,dfn[x],dfn[x]+siz[x]-1)), Mdf(1,1,n,dfn[x],dfn[x]+siz[x]-1,0);
}
return 0;
}