树链剖分,一学就会!全网最通俗易懂、图文并茂的树链剖分好文!

树链剖分是一种用于优化树结构上操作的数据结构,通过轻重链剖分将树分为多条链,简化单点修改、区间修改、区间最值查询等操作。本文通过实例深入浅出地讲解了树链剖分的概念、性质及应用,包括LCA问题的求解和结合线段树进行区间查询与更新。预处理和实际操作的时间复杂度分别为O(n)和O(logn)。
摘要由CSDN通过智能技术生成


链剖分,指对树的边进行划分的一类操作,目的是减少在链上修改、查询等操作的复杂度。链剖分有三类:轻重链剖分、虚实链剖分和长链剖分。

树链剖分的思想是通过轻重链剖分将树分为多条链,保证每个节点都属于且只属于一条链。树链剖分是轻重链剖分,节点到重儿子(子树节点数最多的儿子)之间的路径为重链。每条重链都相当于一段区间,把所有重链首尾相接组成一个线性节点序列,再通过数据结构(如树状数组、SBT、伸展树、线段树等)来维护即可。

size[u]表示以u为根的子树的节点个数,则在u的所有儿子中,size最大的儿子就是重儿子,而u的其他儿子都是轻儿子,当前节点与其重儿子之间的边就是重边,多条重边相连为一条重链。一棵树如下图所示。长度大于1的重链有两条:1-3-6-82-5,单个轻儿子可被视作一个长度为1的重链:47,因此本题中有4条重链。图中深色的节点是重儿子,加粗的边是重边。

 重要性质:

  • v是轻儿子,uv的父节点,则size[v]≤size[u]/2;
  • 从根到某一点路径上,不超过log2n条重链,不超过log2n条轻边。

树链剖分支持以下操作。

1)单点修改:修改一个点的权值。

2)区间修改:修改节点uv路径上节点的权值。

3)区间最值查询:查询节点uv路径上节点的最值。

4)区间和查询:查询节点uv路径上节点的和值。

树链剖分的应用比倍增更广泛,倍增可以做的,树链剖分一定可以做,反过来则不行。树链剖分的代码复杂度不算特别高,调试也不难,树链剖分在算法竞赛中是必备知识。

1.预处理

树链剖分可以采用两次深度优先搜索实现。

1次深度优先搜索维护4个信息:dep[]fa[]size[]son[]

  • dep[u]:u的深度。
  • fa[u]:u的父节点。
  • size[u]:以u为根的子树的节点数。
  • son[u]:u的重儿子,u-son[u]为重边。

2次深度优先搜索以优先走重边的原则,维护3个信息:top[]id[]rev[]

  • top[u]:u所在的重链上的顶端节点编号(重链上深度最小的节点)。
  • id[u]:u在节点序列中的位置下标。
  • rev[x]:树链剖分后节点序列中第x个位置的节点。

id[]rev[]是互逆的。例如,节点u在节点序列中的位置下标是x,则节点序列中第x个位置的节点是uid[u]=xrev[x]=u。对上面的树进行树链剖分后,将所有重链都放在一起组成一个节点序列:[1,3,6,8],[7],[2,5],[4]。序列中第4个位置是8号节点,8号节点的存储下标是4,即rev[4]=8id[8]=4预处理的时间复杂度O(n)

2求解LCA问题

对于LCA(最近公共祖先)问题,点和边均没有权值,因此无须维护线段树来实现。输入树后,先进行树链剖分预处理。

算法代码:

void dfs1(int u,int f) {//求dep、fa、size和son
    size[u]=1;
    for(int i=head[u];i;i=e[i].next){
        int v=e[i].to;
        if(v==f)//父节点 
            continue;
        dep[v]=dep[u]+1;//深度 
        fa[v]=u;
        dfs1(v,u);
        size[u]+=size[v];
        if(size[v]>size[son[u]])
            son[u]=v;
    }
}

void dfs2(int u) {//求top
    if(u==son[fa[u]])
        top[u]=top[fa[u]];
    else
        top[u]=u;
    for(int i=head[u];i;i=e[i].next){
        int v=e[i].to;
        if(v!=fa[u])
            dfs2(v);
    }
}

显然,树中的任意一对节点(u,v)只存在两种情况:①在同一条重链上(top[u]=top[v]);②不在同一条重链上。

对第1种情况,LCA(u,v)就是uv中深度较小的节点。例如下图中求节点38的最近公共祖先时,因为38在同一条重链上且3的深度较小,因此LCA(3,8)=3

对第2种情况,只要想办法将uv两点转移到同一条重链上即可。首先求出uv所在重链的顶端节点top[u]top[v],将其顶端节点深度大的节点上移,直到uv在同一条重链上,再用第1种情况中的方法求解即可。

例如下图中求节点78的最近公共祖先,78不在同一条重链上,先求两个节点所在重链的顶端节点:top[7]=7top[8]=1dep[1]<dep[7]7的顶端节点深度大,因此将v7上移到其父节点3,此时38在同一条重链上,且3的深度较小,因此LCA(7,8)=3

57的最近公共祖先,57不在同一条重链上,先求两节点所在重链的顶端节点:top[5]=2top[7]=7dep[2]<dep[7]7的顶端节点深度大,因此将v7上移到其顶端节点的父节点3

  3所在重链的顶端节点:top[3]=1dep[1]<dep[2]5的顶端节点深度大,因此将u5上移到其顶端节点的父节点1,此时13在同一条重链上,且1的深度较小,因此LCA(5,7)=1

算法代码:

int LCA(int u,int v) {//求区间u、v的最近公共祖先 
	while(top[u]!=top[v]) {//不在同一条重链上
		if(dep[top[u]]>dep[top[v]])//将顶端节点深度大的上移
			u=fa[top[u]];
		else
			v=fa[top[v]];
	}
	return dep[u]>dep[v]?v:u;//返回深度小的节点 
}

3树链剖分与线段树

若在树中进行点更新、区间更新、区间查询等操作,则可以使用线段树来维护和处理。

一棵树如下图所示。

  树链剖分之后的节点序列和下标序列如下图所示。

节点序列对应的权值如下图所示。

  根据w[]序列创建线段树,如下图所示。

  查询节点uv路径上节点权值的最值与和值的方法如下。

  • uv在同一条重链上,则在线段树上查询其对应的下标区间[id[u],id[v]]即可。
  • uv不在同一条重链上,则一边查询,一边将uv向同一条重链上移,然后采用上面的方法处理。对于顶端节点深度大的节点,先查询其到顶端节点的区间,然后一边上移一边查询,直到上移到同一条重链上,再查询在同一条重链上的区间。

查询节点69权值的最值与和值(包括69节点),过程如下。

1)读取top[6]=1top[9]=2,两者不相等则说明其不在一条重链上,且top[9]的深度大,先查询top[9]9之间的最值与和值。

首先得到节点29对应的节点序列下标79

  然后在线段树中查询[7,9]区间的最值与和值。[7,9]区间的最值与和值:Max=15Sum=22

2)将u上移到top[9]2号节点)的父节点,即1号节点,此时16在同一条链上。

  节点16对应的线段树下标为13

  在线段树中查询到[1,3]区间的最值与和值分别为2031,如下图所示。再与前面的结果求最大值与和值,则Max=max(Max,20)=max(15,20)=20Sum=Sum+31=22+31=53

  区间更新的方法与此类似,若不在一条链上,则一边更新,一边向同一条链上靠,最后在同一条链上更新即可。

注意:更新和查询时均需要先得到节点对应的线段树下标,再在线段树上更新和查询。

算法代码:

void dfs1(int u,int f) {//求dep、fa、size、son
	size[u]=1;
	for(int i=head[u];i;i=e[i].next){
		int v=e[i].to;
		if(v==f)//父节点 
			continue;
		dep[v]=dep[u]+1;//深度 
		fa[v]=u;
		dfs1(v,u);
		size[u]+=size[v];
		if(size[v]>size[son[u]])
			son[u]=v;
	}
}

void dfs2(int u,int t){//求top、id、rev
	top[u]=t;
	id[u]=++total;  //u对应的节点序列中的下标
	rev[total]=u;	  //节点序列下标对应的节点u
	if(!son[u])
		return;
	dfs2(son[u],t);//沿着重儿子深度优先搜索
	for(int i=head[u];i;i=e[i].next){
		int v=e[i].to;
		if(v!=fa[u]&&v!=son[u])
			dfs2(v,v);
	}
}

void build(int k,int l,int r){//创建线段树,k表示存储下标,区间为[l,r]
	tree[k].l=l;
	tree[k].r=r;
	if(l==r){
		tree[k].mx=tree[k].sum=w[rev[l]];
		return;
	}
	int mid,lc,rc;
	mid=(l+r)/2;//划分点 
	lc=k*2;  //k节点的左子节点存储下标 
	rc=k*2+1;//k节点的右子节点存储下标 
	build(lc,l,mid);
	build(rc,mid+1,r);
	tree[k].mx=max(tree[lc].mx,tree[rc].mx);//节点的最大值等于左右子节点最值的最大值 		tree[k].sum=tree[lc].sum+tree[rc].sum;//节点的和值等于左右子树的和值 
}

void query(int k,int l,int r){//求[l,r]区间的最值、和值 
	if(tree[k].l>=l&&tree[k].r<=r) {//找到该区间
		Max=max(Max,tree[k].mx);
		Sum+=tree[k].sum;
		return;
	} 
	int mid,lc,rc;
	mid=(tree[k].l+tree[k].r)/2;//划分点 
	lc=k*2;  	//左子节点存储下标 
	rc=k*2+1;	//右子节点存储下标
	if(l<=mid)
		query(lc,l,r);//到左子树中查询 
	if(r>mid)
		query(rc,l,r);//到右子树中查询
}

void ask(int u,int v){//求u、v之间的最值或和值
	while(top[u]!=top[v]) {//不在同一条重链上
		if(dep[top[u]]<dep[top[v]])
			swap(u,v);
		query(1,id[top[u]],id[u]);//u顶端节点和u之间 
		u=fa[top[u]];
	}
	if(dep[u]>dep[v])	//在同一条重链上 
		swap(u,v); 	//深度小的节点为u
	query(1,id[u],id[v]);
}

void update(int k,int i,int val){//u对应的下标i,将其值更新为val 
	if(tree[k].l==tree[k].r&&tree[k].l==i){//找到i
		tree[k].mx=tree[k].sum=val;
		return;
	}
	int mid,lc,rc;
	mid=(tree[k].l+tree[k].r)/2;//划分点 
	lc=k*2;  	//左子节点存储下标 
	rc=k*2+1;	//右子节点存储下标
	if(i<=mid)
		update(lc,i,val);//到左子树中更新 
	else
		update(rc,i,val);//到右子树中更新
	tree[k].mx=max(tree[lc].mx,tree[rc].mx);//返回时更新最值
	tree[k].sum=tree[lc].sum+tree[rc].sum;//返回时更新和值
}

算法分析:树链剖分预处理需要O(n)时间,每次更新和查询都需要O(logn)时间。

原创声明:本文来自本人著作《算法训练营:海量图解+竞赛刷题》进阶篇,未经许可,谢绝转载。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

趣学算法

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值