树链剖分简析

树链剖分

定义(来自百度百科):

树链剖分,计算机术语,指一种对树进行划分的算法,它先通过轻重边剖分将树分为多条链,保证每个点属于且只属于一条链,然后再通过数据结构(树状数组、BST、SPLAY、线段树等)来维护每一条链。

那么,树链剖分有什么用呢?
  • 将树从x到y结点最短路径上所有节点的值都加上z
  • 求树从x到y结点最短路径上所有节点的值之和
  • 将以x为根节点的子树内所有节点值都加上z
  • 求以x为根节点的子树内所有节点值之和
总而言之,树链剖分可以解决树上路径修改及整棵子树的修改等操作。
但如何实现这些操作呢?
常见的路径剖分的方法是轻重树链剖分(启发式剖分)
  • 对于这棵树的任一有至少一个孩子的节点,定义它的重儿子为以其子节点为根的子树最大的树的根节点(若有不止一个重儿子,则先遍历到的为重儿子)。
  • 对于一个节点,其除重儿子外所有的节点都为其轻儿子。
  • 而该节点与其重儿子的连线就会形成一条重边。其余的边为轻边。
  • 相邻重边的连线为重链。
放图理解一下:

Alt

其中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旅行
月下“毛景树”
……

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值