树链剖分是什么,能吃吗?

树剖的思维难度不大,但一般比较难调。

引入

树链剖分就是把一棵树剖成一条条链,视剖的方式分为长链剖分与重链剖分。

所谓重剖,把子树大小最大的那个儿子称为“重儿子”,把树剖为若干条重链,长剖类似,最长的儿子为“长儿子”,剖为若干条长链。由于重剖应用范围比较广(下面会讲),而长剖的用途主要就是维护一些按深度转移、算贡献的 DP,所以平时所说的树剖一般就是重剖。

红边为重边,重剖的效果大致如下:

我们都知道求 LCA 可以用倍增、 Tarjan、ST表求,但其实也可以用树剖求,这里用求 LCA 来引入树剖。

再解释几个定义:

  1. 轻儿子:不是重儿子就是轻儿子。

  2. 重边:连接两个重儿子的边。

  3. 重链:连续的重边连成的一条链。

  4. 轻边:连接两个轻儿子的边。

  5. 链头:一条重链深度最浅的点为链头,必定为轻儿子(如果不是就会接上其他重链,显然不对)。

如上图,树剖可以把树分为为从根到叶子的若干条不相交的链,其最关键的性质就是是从任一点出发,到达根节点经过的重链(轻边)不超过 logn 条。

在求 u,v 的 LCA 时,设有两种情况:

  1. 两点不在一条重链上,不妨设 u 的深度较深,一路向上跳,每次跳到链头的父亲。
  2. 在一条重链上,则深度更浅的点为 LCA。
int lca(int u,int v) {
	while(tr[u].top!=tr[v].top) {
		if(tr[tr[u].top].dep<tr[tr[v].top].dep) swap(u,v);//不妨设u的深度更深,好让u往上跳
		u=tr[tr[u].top].fa;//越过轻边,到下一条重链
	}
	if(tr[u].dep>tr[v].dep) swap(u,v);
	return u;
}

接下来,需要求出以上所需要的数组。

struct trr {
	//当前节点为u
	//tr[u].(...)
	int son;//u的重儿子
	int siz;//以u为根子树大小
	int dep;//u的深度
	int fa;// u的父亲
	/*********************/
	int id;//u的dfn序,后面要用
	int top;//u所在链的链顶
} tr[];

我们需要两个 DFS 来求出以上的数组,上方出示代码的前四个在 dfs1() 中求出,后两个在 dfs2() 中求出。

int dfs1(int u,int fa) {
	tr[u].fa=fa,tr[u].dep=tr[fa].dep+1,tr[u].siz=1;
	int mx=-1;
	for(int i=head[u]; i; i=edge[i].nxt) {
		int v=edge[i].to;
		if(v==fa)continue;
		tr[u].siz+=dfs1(v,u); 
		if(tr[v].siz>mx) tr[u].son=v,mx=tr[v].siz;//更新重儿子 
	}
	return tr[u].siz;
}
void dfs2(int u,int tp) {
	//tr[u].id=++tot;求dfn序后面用得上 
	//a[tot]=wi[u];赋点权求LCA暂时也用不上 
	tr[u].top=tp;//当前链的链顶 
	if(!tr[u].son) return ;
	dfs2(tr[u].son,tp);//优先重儿子 
	for(int i=head[u]; i; i=edge[i].nxt) {
		int v=edge[i].to;
		if(tr[v].id)continue;
		dfs2(v,v);
	}
}

求出这些后,就可用树剖求 LCA 了,可以去 A 掉LCA的板子了。

树链剖分的真正用法

树剖的用途当然不止于求 LCA ,各种进阶用法依赖于一个十分重要的性质:一条重链内部 dfn 序是连续的。

也就是说,求了 dfn 序,才可以完全的“剖”开这棵树,把树上问题转换为线段上的区间问题,线段树正好可以维护。

树剖可以解决不少问题,比较经典的有:

  1. 修改(查询) uv 的路径上的点权(和)
  2. 修改(查询)以 u 为根的子树各点点权(和)。

下面以查询 uv 的简单路径上的点权和为例分析一下树剖是如何实现上述功能的。

还是这一张图为例,不过先把他的 dfn 序跑出来。

按照上文中 dfs2() 的方式跑出来的 dfn 序保证了链内部 dfn 序是连续的,事实上,我们可以把哪些没有连在任何一条重链上的叶子结点视作是单独的一条重链。

比如说,现在要求 9 到 11 的路径上的权值和,则我们先找出来节点深度较深的那个,然后向上跳到链顶。

(图画的太抽象了)

跳的时候,由于一条重链内的 dfn 序是连续的,用线段树查询一下权值和即可。

然后越过一条轻边(也就是在跳到链头的父亲)。

然后重复以上过程,不断让深度大的那个点向上跳,并用线段树查询,直到两者位于同一条链。

最后一步查询,都已跳到同一链上时,不妨设 u 深度较浅,我们查询一下 tr[u].idtr[v].id 的权值和即可(现在在一条链上,dfn 序自然是连续的)。

修改同理。

查询修改子树时可以更简单一点,由于子树内部的 dfn 序也是连续的,所以我们查询(修改)以 u 为根的子树时,可以直接从 tr[u].id 询问(修改)到 tr[u].id+tr[u]s.iz-1

至此,树剖的基本操作完工,可以去过掉板子

ACcode
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+10;
struct node {
	int nxt,to;
} edge[maxn<<1];
int head[maxn<<1],cnt=0,tot=0;
int n,m,rt,mod;
struct trr {
	//当前节点为u
	//tr[u].(...)
	int son;//u的重儿子
	int siz;//以u为根子树大小
	int dep;//u的深度
	int fa;// u的父亲
	/************/
	int id;//u的dfn序,后面要用
	int top;//u所在链的链顶
} tr[maxn];
struct seg {
	int l,r,sum,add,siz;
} t[maxn<<2];
int wi[maxn],a[maxn];
void add(int u,int v) {
	edge[++cnt].to=v;
	edge[cnt].nxt=head[u];
	head[u]=cnt;
}
void pushdown(int p) {
	if(!t[p].add)return;
	t[p*2].sum=(t[p*2].sum+t[p*2].siz*t[p].add)%mod;
	t[p*2+1].sum=(t[p*2+1].sum+t[p*2+1].siz*t[p].add)%mod;
	t[p*2].add=t[p*2].add+t[p].add;
	t[p*2+1].add+=t[p].add;
	t[p].add=0;
}
void build(int p,int l,int r) {
	t[p].l=l,t[p].r=r,t[p].siz=r-l+1;
	if(l==r) {
		t[p].sum=a[l]%mod;
		return ;
	}
	int mid=(l+r)/2;
	build(p*2,l,mid);
	build(p*2+1,mid+1,r);
	t[p].sum=(t[p*2].sum+t[p*2+1].sum)%mod;
}
void add(int p,int l,int r,int k) {
	if(t[p].l>=l && r>=t[p].r) {
		t[p].sum=(t[p].sum+t[p].siz*k)%mod;
		t[p].add+=k;
		return ;
	}
	pushdown(p);
	int mid=(t[p].l+t[p].r)/2;
	if(l<=mid)add(p*2,l,r,k);
	if(r>mid) add(p*2+1,l,r,k);
	t[p].sum=(t[p*2].sum+t[p*2+1].sum)%mod;
}
int ask(int p,int l,int r) {
	if(t[p].l>=l && t[p].r<=r)
		return t[p].sum;
	pushdown(p);
	int ans=0;
	int mid=(t[p].r+t[p].l)>>1;
	if(l<=mid) ans=(ans+ask(p*2,l,r))%mod;
	if(r>mid) ans=(ans+ask(p*2+1,l,r))%mod;
	return ans;
}
//**********************************************************************//
int dfs1(int u,int fa) {
	tr[u].fa=fa,tr[u].dep=tr[fa].dep+1,tr[u].siz=1;
	int mx=-1;
	for(int i=head[u]; i; i=edge[i].nxt) {
		int v=edge[i].to;
		if(v==fa)continue;
		tr[u].siz+=dfs1(v,u); 
		if(tr[v].siz>mx) tr[u].son=v,mx=tr[v].siz;//更新重儿子 
	}
	return tr[u].siz;
}
void dfs2(int u,int tp) {
	tr[u].id=++tot;
	a[tot]=wi[u];
	tr[u].top=tp;//当前链的链顶 
	if(!tr[u].son) return ;
	dfs2(tr[u].son,tp);//优先重儿子 
	for(int i=head[u]; i; i=edge[i].nxt) {
		int v=edge[i].to;
		if(tr[v].id)continue;
		dfs2(v,v);
	}
}
void treeadd(int u,int v,int k) {
	while(tr[u].top!=tr[v].top) {
		if(tr[tr[u].top].dep<tr[tr[v].top].dep) swap(u,v);
		add(1,tr[tr[u].top].id,tr[u].id,k);
		u=tr[tr[u].top].fa;
	}
	if(tr[u].dep>tr[v].dep)
		swap(u,v);
	add(1,tr[u].id,tr[v].id,k);
}
void treesum(int u,int v) {
	int ans=0;
	while(tr[u].top!=tr[v].top) {
		if(tr[tr[u].top].dep<tr[tr[v].top].dep) swap(u,v);
		ans+=ask(1,tr[tr[u].top].id,tr[u].id);
		u=tr[tr[u].top].fa;
	}
	if(tr[u].dep>tr[v].dep)
		swap(u,v);
	ans=(ans+ask(1,tr[u].id,tr[v].id))%mod;
	cout<<ans<<endl;
}
int main() {
	cin>>n>>m>>rt>>mod;
	for(int i=1; i<=n; i++)
		cin>>wi[i];
	for(int u,v,i=1; i<=n-1; i++)
		cin>>u>>v,add(u,v),add(v,u);
	int o=dfs1(rt,0);
	dfs2(rt,rt);
	build(1,1,n);
	while(m--) {
		int op,x,y,z;
		cin>>op;
		if(op==1)cin>>x>>y>>z,treeadd(x,y,z);
		if(op==2)cin>>x>>y,treesum(x,y);
		if(op==3)cin>>x>>z,add(1,tr[x].id,tr[x].id+tr[x].siz-1,z);
		if(op==4) {
			cin>>x;
			cout<<ask(1,tr[x].id,tr[x].id+tr[x].siz-1)<<endl;
		}

	}
	return 0;
}
其他小技巧

很多时候题目给我们的是边权而非点权,那该怎办。

这里有一个小技巧,我们都知道每个节点至多只可能有一个父亲,所以我们将一个点与他父亲的边之边权转为他的点权即可。

图片待施工。

具体到题目中,一般会给出边的编号进行查询、修改,我们只要判断一下两点的深度深浅即可判断该用谁的点权。

但在查询修改时,我们发现,对于某两个点的 LCA 的点权根本不是原先路径上的边权,所以我们要扣去这个点,其实好办,在查询“跳链”最后两者都在一条链上时,我们设点 u 深度较浅,然后将dfn序加一即可,落实到代码中就是 tr[u].id+1

P1505 [国家集训队] 旅游就是一道不错的边权转点权的题目,区间取反要考虑好标记之间的转换。

ACcode

结语

学了树剖之后就可以水一大堆蓝紫题了

posted @   p7gab  阅读(10)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示