树链剖分(重链剖分)

前言

本文主要针对于还没有学习过这个算法的新手同学,因为我也刚学
最好要先知道DFN序,LCA是什么。

一、什么是树链剖分,是干什么的?

相较于树上的问题,我们通常对线性的结构更为敏感,因此我们希望将树上的问题转化为线性的结构来去解决问题。
树链剖分用于将树分割成若干条链的形式,以维护树上路径的信息
具体来说,将整棵树剖分为若干条链,使它组合成线性结构,然后用其他的数据结构维护信息。
树链剖分一般有重链剖分和长链剖分,一般来讲都是指重链剖分,本文也用重链剖分来讲解。

二、一般的树链剖分操作(重链剖分)

1.前置知识

我们给出一些定义:
重子节点 表示其子节点中子树最大的子结点。如果有多个子树最大的子结点,取其一。如果没有子节点,就无重子节点。

定义 轻子节点 表示剩余的所有子结点。

从这个结点到重子节点的边为 重边。

到其他轻子节点的边为 轻边。

若干条首尾衔接的重边构成 重链。

把落单的结点也当作重链,那么整棵树就被剖分成若干条重链。

可能看着有点晕,没关系,结合图来看一下:
图片来自oiwiki我们可以看到图片中灰色的点就是他父节点中的重儿子,也就是父节点的子树大小最大的那个点。右边的图片就是我们树剖结束后的效果。
怎么实现的呢?
这里我们主要分为两个步骤:
1.预处理出每个结点的子树大小,并且利用这个大小找到每个结点的重儿子
2.进行划分,处理出每个节点属于哪条划分出来的重链(也就是头节点是谁)。

int top[N],siz[N],hson[N];  
//top用来记录每条重链的头节点  
//siz表示这个点的子树大小  
//hson[x]表示x节点的重儿子是谁  
void dfs1(int x,int f) {  
    siz[x]=1;  
    for(int i=0;i<g[x].size();++i) {  
        int v=g[x][i];  
        if(v==f) continue;  
        dfs1(v,x);  
        siz[x]+=siz[v];  
        if(siz[hson[x]]<siz[v]) hson[x]=v;  
    }  
} 

至此我们已经得到了每个点的重儿子是谁,然后就可以进行划分了。
接下来我们再来看一下重链剖分的性质:

树上每个节点都属于且仅属于一条重链。

重链开头的结点不一定是重子节点(因为重边是对于每一个结点都有定义的)。

所有的重链将整棵树 完全剖分。

在剖分时 重边优先遍历,最后树的 DFS 序上,重链内的 DFS 序是连续的。按 DFN 排序后的序列即为剖分后的链。(如果还不了解DFN序的建议去了解一下再看)。
因此对于一个点如果他有重儿子就先遍历它的重儿子,然后回溯回去如果还有其它不是重儿子的点再接着走这个点,然后这个点有重儿子再先走它的重儿子…具体看代码:

void dfs2(int x,int tp) {
	top[x]=tp;
	if(hson[x]) {
		dfs2(hson[x],tp);
	}
	for(int i=0;i<g[x].size();++i) {
		int v=g[x][i];
		if(v==fa[x]||v==hson[x]) continue;
		dfs2(v,v);
	}
}

至此树链剖分的基本操作就结束了,但是这有什么用?划分完了我们用来干嘛?

2.树链剖分求LCA

怎么利用树剖来求LCA(最近公共祖先)呢?

不断向上跳重链,当跳到同一条重链上时,深度较小的结点即为 LCA。

向上跳重链时需要先跳所在重链顶端深度较大的那个。
在这个求LCA的过程中“向上跳”就是我们树链剖分的精髓所在了。(虽然向上跳的方式和倍增不同,但如果理解了倍增可能会对这个过程理解的更快)。
具体过程就是:首先判断两个点在不在一条重链上,如果不在的话,我们让头节点深度大(更往下)的那个点向上跳到他头节点的父节点(这里自己结合上面的图片想一想会更好理解),然后当跳到一条链上的时候深度较小的点就是他们的LCA。

可以看一下代码理解一下,题目是洛谷最近公共祖先的模板(P3379)。
(n个点,n-1条边,m次询问,给一个根节点)。

#include<iostream>
#include<math.h>	
#include<string.h>
#include<vector>
#include<queue>
#include<algorithm>
#include<set>
#include<stack>
#include<map>
#include<unordered_map>
#define debug(a) cout<<"***"<<a<<"***\n"
using namespace std;
typedef long long ll;
#define INF 0x3f3f3f3f
constexpr int N=5e5+10;
constexpr int mod=1e9+7;
int n,m,root;
vector<int>g[N];
int top[N],siz[N],hson[N],dep[N],fa[N];
void dfs1(int x,int f) {
	siz[x]=1;
	fa[x]=f;
	dep[x]=dep[f]+1;
	for(int i=0;i<g[x].size();++i) {
		int v=g[x][i];
		if(v==f) continue;
		dfs1(v,x);
		siz[x]+=siz[v];
		if(siz[hson[x]]<siz[v]) hson[x]=v;
	}
}
void dfs2(int x,int tp) {
	top[x]=tp;
	if(hson[x]) {
		dfs2(hson[x],tp);
	}
	for(int i=0;i<g[x].size();++i) {
		int v=g[x][i];
		if(v==fa[x]||v==hson[x]) continue;
		dfs2(v,v);
	}
}

int lca(int a,int b) {
	while(top[a]!=top[b]) {
		if(dep[top[a]]<dep[top[b]]) swap(a,b);
		a=fa[top[a]]; 
	}
	return dep[a]<dep[b]?a:b;
}
int main() {
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    cin>>n>>m>>root;
    for(int i=1;i<=n-1;++i) {
    	int u,v;cin>>u>>v;
    	g[u].emplace_back(v);
    	g[v].emplace_back(u);
	}
	dfs1(root,0);
	dfs2(root,root);
	for(int i=1;i<=m;++i) {
		int u,v;cin>>u>>v;
		cout<<lca(u,v)<<'\n';
	}
	return 0;
}


3.用其他数据结构来维护

这个时候我们已经对树链剖分有一个基本的认识了,那么怎么做到的前面说的用其他数据结构去维护呢?
这个时候DFN序就起了很大的作用了,如果已经对DFN序有了解的话那么应该已经知道了对于一颗子树上的DFN序是连续的。(不了解也别怕,DFN序就是按照dfs的顺序跑一遍给节点一个标号 )。
那么如果对于每次操作都更改子树的话我们是不是就可以利用这个DFN序去建一个数据结构(比如线段树)去维护上面的操作呢,比如说将一个号码的全部子树都加或减去一个数字,我们就可以用DFN序的连续性用线段树来 维护,那么如果换一下问题不是全部子树,而是从x点到y点呢?
我们来想一下,这个时候就可以用到我们的树剖。

我们可以按照他dfs2中的顺序(先走重儿子)去建一个DFN序,用这个DFN序去建一个线段树,这时候我们可以想一想,如果x和y在一条重链上,那么直接维护x到y的DFN序的这一段数字,否则的话,想一下刚才我们求LCA是怎么做的,同样我们可以先在他们不属于同一条重链的时候去跳,如果不在一条链上,那我们就先找到头节点深度更大(在树上更往下)的点去维护,(比如说top[x]深度更大我们就先维护top[x]->x的DFN序),然后将x跳到头结点的父节点(fa[top[x]])。这样不断去维护,直到他们在一条重链。
例题有洛谷的P3384
代码如下:

#include<iostream>
#include<math.h>
#include<string.h>
#include<vector>
#include<queue>
#include<algorithm>
#include<set>
#include<stack>
#include<map>
#include<unordered_map>
#define debug(a) cout<<"***"<<a<<"***\n"
using namespace std;
typedef long long ll;
#define int long long
#define INF 0x3f3f3f3f
constexpr int N=2e5+10;
constexpr int mod=1e9+7;
struct Node {
	int sum,l,r,lz;
}tree[N<<2];
vector<int>g[N];
int a[N],dfn[N],todfn[N],dep[N],fa[N],siz[N],hson[N],top[N];
int n,m,r,p;
int id;
void dfs1(int x,int f) {
	siz[x]=1;
	fa[x]=f;
	dep[x]=dep[f]+1;
	
	
	for(int i=0;i<g[x].size();++i) {
		if(g[x][i]!=f) {
			dfs1(g[x][i],x);
			siz[x]+=siz[g[x][i]];
			if(siz[hson[x]]<=siz[g[x][i]]) hson[x]=g[x][i];
		}
	}
}
void dfs2(int x,int tp) {
	top[x]=tp;
	dfn[x]=++id;
	todfn[id]=x;
	if(hson[x]) dfs2(hson[x],tp);
	else return;
	for(int i=0;i<g[x].size();++i) {
		if(g[x][i]==fa[x]||g[x][i]==hson[x]) continue;
		dfs2(g[x][i],g[x][i]);
	}
}
void push_up(ll i) {
	tree[i].sum=(tree[i<<1].sum%p+tree[i<<1|1].sum%p)%p;
}
void build(ll i,ll l,ll r) {
	tree[i].l=l;
	tree[i].r=r;
	if(l==r) {
		tree[i].sum=a[todfn[l]]%p;
		return;
	}
	int mid=(l+r)>>1;
	build(i<<1,l,mid);
	build(i<<1|1,mid+1,r);
	push_up(i);
}
void push_down(ll i) {
	if(tree[i].lz) {
		tree[i<<1].sum=(tree[i<<1].sum%p+(tree[i].lz%p*(tree[i<<1].r-tree[i<<1].l+1))%p)%p;
		tree[i<<1|1].sum=(tree[i<<1|1].sum%p+(tree[i].lz%p*(tree[i<<1|1].r-tree[i<<1|1].l+1))%p)%p;
		tree[i<<1].lz=(tree[i<<1].lz%p+tree[i].lz%p)%p;
		tree[i<<1|1].lz=(tree[i<<1|1].lz%p+tree[i].lz%p)%p;
		tree[i].lz=0;
	}
}
void add(ll i,ll l,ll r,ll k) {
	if(tree[i].l>=l&&tree[i].r<=r) {
		tree[i].sum=(tree[i].sum%p+k%p*(tree[i].r-tree[i].l+1)%p)%p;
		tree[i].lz=(tree[i].lz%p+k%p)%p;
		return;
	}
	push_down(i);
	int mid=(tree[i].l+tree[i].r)>>1;
	if(l<=mid) add(i<<1,l,r,k);
	if(r>=mid+1) add(i<<1|1,l,r,k);
	push_up(i);
}
ll query(ll i,ll l,ll r) {
	ll ans=0;
	if(tree[i].l>=l&&tree[i].r<=r) {
		return tree[i].sum%p;
	}
	push_down(i);
	int mid=(tree[i].r+tree[i].l)>>1;
	if(l<=mid) ans=(ans%p+query(i<<1,l,r)%p)%p;
	if(r>=mid+1) ans=(ans%p+query(i<<1|1,l,r)%p)%p;
	return ans;
}
void update(ll x,ll y,ll z) {
	while(top[x]!=top[y]) {
		if(dep[top[x]]<dep[top[y]]) swap(x,y);
		add(1,dfn[top[x]],dfn[x],z);
		x=fa[top[x]];
	}
	if(dep[x]<dep[y]) swap(x,y);
	add(1,dfn[y],dfn[x],z);
}
ll get(ll x,ll y) {
	ll ans=0;
	while(top[x]!=top[y]) {
		if(dep[top[x]]<dep[top[y]]) swap(x,y);
		ans=(ans%p+query(1,dfn[top[x]],dfn[x])%p)%p;
		x=fa[top[x]];
	}
	if(dep[x]<dep[y]) swap(x,y);
	ans=(ans%p+query(1,dfn[y],dfn[x])%p)%p;
	return ans;
}
signed main() {
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	cin>>n>>m>>r>>p;
	for(int i=1;i<=n;++i) {
		cin>>a[i];
	}
	for(int i=1;i<=n-1;++i) {
		int u,v;
		cin>>u>>v;
		g[u].emplace_back(v);
		g[v].emplace_back(u);
	}
	dfs1(r,0);
	dfs2(r,r);
	build(1,1,n);
	while(m--) {
		ll op,x,y,z;
		cin>>op;
		if(op==1) {
			cin>>x>>y>>z;
			update(x,y,z);
		}else if(op==2) {
			cin>>x>>y;
			cout<<get(x,y)<<'\n';
		}else if(op==3) {
			cin>>x>>z;
			add(1,dfn[x],dfn[x]+siz[x]-1,z);
		}else if(op==4) {
			cin>>x;
			cout<<query(1,dfn[x],dfn[x]+siz[x]-1)<<'\n';
		}
	}
	return 0;
}
 

4.时间复杂度

可以发现,当我们向下经过一条 轻边 时,所在子树的大小至少会除以二。

因此,对于树上的任意一条路径,把它拆分成从 LCA 分别向两边往下走,分别最多走 [O(log n)] 次,因此,树上的每条路径都可以被拆分成不超过 [O(log n)] 条重链。

  • 48
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值