树链剖分
定义(来自百度百科):
树链剖分,计算机术语,指一种对树进行划分的算法,它先通过轻重边剖分将树分为多条链,保证每个点属于且只属于一条链,然后再通过数据结构(树状数组、BST、SPLAY、线段树等)来维护每一条链。
那么,树链剖分有什么用呢?
- 将树从x到y结点最短路径上所有节点的值都加上z
- 求树从x到y结点最短路径上所有节点的值之和
- 将以x为根节点的子树内所有节点值都加上z
- 求以x为根节点的子树内所有节点值之和
- …
总而言之,树链剖分可以解决树上路径修改及整棵子树的修改等操作。
但如何实现这些操作呢?
常见的路径剖分的方法是轻重树链剖分(启发式剖分)
- 对于这棵树的任一有至少一个孩子的节点,定义它的重儿子为以其子节点为根的子树最大的树的根节点(若有不止一个重儿子,则先遍历到的为重儿子)。
- 对于一个节点,其除重儿子外所有的节点都为其轻儿子。
- 而该节点与其重儿子的连线就会形成一条重边。其余的边为轻边。
- 相邻重边的连线为重链。
放图理解一下:
其中0的重儿子是1,1的重儿子是3,3的重儿子是4……
红色的边是重边,黑色的边是轻边
0-4,6-8 为重链
性质:(1)轻边(U,V),size(V)<=size(U)/2。
(2)从根到某一点的路径上,不超过logN条轻边,不超过logN条重路径。
- 证明(1):因为U的重儿子的size肯定大于等于U/2(由定义),所以V的size最大为U/2。
- 证明(2):由(1)中结论得:若一个点从一条轻边跳到其父节点上,则其size至少增加1倍,由1增加到N至多需要增加logN次,故轻边至多logN条。而任意两条重链间有至少一条轻边连接(否则就是一条重链),则最多比轻边多一条,故也是logN级别。
实现方式: 2个dfs
第一个dfs:
- 找到每个节点的父亲
- 找到每个节点的重儿子
- 求出每个节点的子树大小
- 求出每个节点的深度
第二个dfs:
- 找到每个节点所在重链的顶点
- 找到每个节点在线段树等数据结构中的编号
- 找到每个节点在原数组中的编号
数组定义:
- father[]:节点的父亲
- son[]:节点的重儿子
- size[]:节点的子树大小
- dep[]:节点的深度
- top[]:节点所在重链的顶点
- seg[]:节点在线段树等数据结构中的编号
- rev[]:节点在原数组中的编号,即rev[seg[u]]=u;
代码1
void dfs1(int u,int fa){//当前搜索到的节点及其父节点
father[u]=fa;//求father数组
size[u]=1;//子树大小初始化为1
dep[u]=dep[fa]+1;//节点深度为父亲的深度+1
int e,v;
for(e=first[u];e,v=to[e];e=next[e]){//在邻接表中查询
if(v!=fa){//若不是其父节点
dfs1(v,u);//搜索节点v
size[u]+=size[v];//U的子树大小为其子树大小之和加上自身
if(size[v]>size[son[u]]) son[u]=v;
//若节点v的大小比当前最大的子节点大,则更新其重儿子
}
}
}
代码2
void dfs2(int u,int tp){//当前搜索到的节点及其所在重链顶端
top[u]=tp;//求top数组
seg[u]=++ind;//求u在数据结构中的编号
rev[seg[u]]=u;//求u在原数组中的编号
if(son[u]) dfs2(son[u],tp); //其重儿子与它在同一条重链上
int e,v;
for(e=first[u];e,v=to[e];e=next[e]){
if(!top[v]) dfs2(v,v);//若没有被搜索过,则为其轻儿子
}
}
操作实现
一般放在线段树中查询,因为线段树修改与查询较灵活
在建树时只需改变下列代码,其余操作均不变:
if(l==r){
sum[o]=a[rev[l]];//因为在之前的操作中更新了编号
return ;
}
在进行两点间路程修改时不能单纯地直接改,需要找到其LCA(最近公共祖先),再在路径上进行修改。(这是因为要改的两点编号不一定连续)
查询时不能从两点一步一步跳,应该一条重链一条重链地跳(是因为效率原因)。当两点所在重链的顶端相等时,深度较小的点则为2点LCA,此时查询结束,返回值。(也可用于求LCA)
//此处以查询为例,修改同理,只需要把query改为modify
int Ask(int x,int y){//查询x到y点权之和
int fx=top[x],fy=top[y],ans=0;//找到两点重链顶端
while(fx!=fy){//两点重链顶端不等(未相遇)
if(dep[fx]<dep[fy]) swap(x,y),swap(fx,fy);//选择深度较小的向上跳
ans+=query(1,1,ind,seg[fx],seg[x]);//求该链的值
ans%=p;//取模
x=father[fx];//跳到另一条重链上
fx=top[x];//更新顶点
}
if(dep[x]>dep[y]) swap(x,y);//深度小的为LCA
ans+=query(1,1,ind,seg[x],seg[y]);//查询x-y的值
return ans%p;// 返回值
}
但当进行一棵子树的修改与查询操作时可以直接查询,原因是搜索顺序是向深处搜索,故一棵子树的编号总是连续的。
modify(1,1,ind,seg[x],seg[x]+size[x]-1,w);//从根节点到最后搜索的节点
补充:纯LCA求法
int lca(int x,int y){
int fx=top[x],fy=top[y];
while(fx!=fy){
if(dep[fx]<dep[fy]) swap(fx,fy),swap(x,y);
x=fa[fx],fx=top[x];
}
return (dep[x]<dep[y])?x:y;
}
这样就结束了树链剖分的全部了
题目 :
[模板]洛谷P3384
ZJOI2008树的统计
HAOI2015树上操作
SDOI2014旅行
月下“毛景树”
……