[洛谷P3384][题解]轻重链剖分

0.序

是一道模板题,写这篇题解是为了骗取阅读量把我对树剖的见解写出来+巩固。

1.概述

树链剖分是一种毒瘤算法,她可以用来解决一些树上毒瘤问题。
有这样一群毒瘤出题人,他们把序列上很简单的问题搬到了树上,于是就有了树链剖分。
基本思路是将整棵树剖分成轻重链,然后用毒瘤数据结构分别维护,所以树链剖分(approx)优雅的暴力。
先给出几个定义:
重儿子:一个节点子节点最多的儿子。
轻儿子:除了重儿子之外的儿子。
重边:父亲连向重儿子的边。
轻边:不是重边的边。
重链:重边连起来的链。特别地,两端均连轻边的节点自成一条重链。
轻链:非重链的路径。所有轻链的长度都是1。
还有几个数组:
hvs[]:当前节点的重儿子。
ltp[]:当前节点所在重链的顶端(深度最小)。

两次DFS:
第一次:求fa、dep、siz等基本数组和hvs。hvs可以直接DFS每个节点比较求出,相信学到树剖的大佬已经会了,不多讲。
第二次:求ltp数组。如果当前节点是重儿子,直接继承当前重链头;否则以当前节点为重链头,继续DFS。

DFS完之后,我们可爱的小树已经被拍扁成DFS序列了,接下来只需用线段树/树状数组等毒瘤数据结构来维护即可。
给出几个例子:
1.更改/查询x到y的路径
只需按照求LCA的步骤,依次上跳。注意树剖的上跳是u=fa[ltp[u]],也就是一次跳一条重链+轻链,可以思考一下她的复杂度。
因为DFS序里一条链上的编号都是连续的,所以直接线段树修改/查询。
给出修改的例子:

while(ltp[u]!=ltp[v]){
	if(dep[ltp[u]]<dep[ltp[v]])swap(u,v);
	Modify(1,idx[ltp[u]],idx[u],add);
	u=fa[ltp[u]];
}
if(dep[u]>dep[v])swap(u,v);
Modify(1,idx[u],idx[v],add);

2.更改/查询x的子树
容易发现,一棵子树在DFS序列里也是连续的,所以直接维护即可。
给出修改的例子:

Modify(1,idx[u],idx[u]+siz[u]-1,add);

具体实现细节可以参考代码。

2.代码

#define N 100010
int n,m,r,p,tot;
int siz[N],fa[N],dep[N],ipt[N];
int hvs[N],ltp[N],idx[N],num[N];
struct Edge {
	int to,nxt;
}e[N<<1];
int head[N],cnt;
inline void ade(int u,int v){
	e[++cnt].to=v;
	e[cnt].nxt=head[u];
	head[u]=cnt;
}
struct Node {
	int l,r,tag,wei;
}tr[N<<2];
void DFS1(int now,int ff){
	fa[now]=ff;
	siz[now]=1;
	dep[now]=dep[ff]+1;
	for(rg int i=head[now];i;i=e[i].nxt){
		int v=e[i].to;
		if(v!=ff){
			DFS1(v,now);
			siz[now]+=siz[v];
			if(siz[v]>siz[hvs[now]])hvs[now]=v;
		}
	}
}
void DFS2(int now,int tp){
	ltp[now]=tp;
	idx[now]=++tot;
	num[tot]=ipt[now];
	if(hvs[now])DFS2(hvs[now],tp);
	for(rg int i=head[now];i;i=e[i].nxt){
		int v=e[i].to;
		if(v!=fa[now]){
			if(v!=hvs[now])DFS2(v,v);
		}
	}
}
void Build(int k,int l,int r){
	tr[k].l=l,tr[k].r=r;
	if(tr[k].l==tr[k].r){
		tr[k].wei=num[l]%p;
		return;
	}
	Build(ls,l,nmid);
	Build(rs,nmid+1,r);
	pushup;
}
inline void Pushdn(int k){
	if(tr[k].tag){
		tr[ls].wei+=tr[k].tag*(tr[ls].r-tr[ls].l+1)%p;
		tr[rs].wei+=tr[k].tag*(tr[rs].r-tr[rs].l+1)%p;
		tr[ls].wei%=p,tr[rs].wei%=p;
		tr[ls].tag+=tr[k].tag%p;
		tr[rs].tag+=tr[k].tag%p;
		tr[ls].tag%=p,tr[rs].tag%=p;
		tr[k].tag=0;
	}
}
void Modify(int k,int l,int r,int add){
	if(l<=tr[k].l&&tr[k].r<=r){
		tr[k].wei+=add%p*(tr[k].r-tr[k].l+1)%p;
		tr[k].tag+=add%p;
		tr[k].wei%=p,tr[k].tag%=p;
		return;
	}
	Pushdn(k);
	if(l<=tmid)Modify(ls,l,r,add);
	if(tmid<r)Modify(rs,l,r,add);
	pushup;
}
int Query(int k,int l,int r){
	if(l<=tr[k].l&&tr[k].r<=r){
		return tr[k].wei%p;
	}
	Pushdn(k);
	int res=0;
	if(l<=tmid)res=(res+Query(ls,l,r))%p;
	if(tmid<r)res=(res+Query(rs,l,r))%p;
	return res;
}
void Opt1(int u,int v,int add){
	add%=p;
	while(ltp[u]!=ltp[v]){
		if(dep[ltp[u]]<dep[ltp[v]])swap(u,v);
		Modify(1,idx[ltp[u]],idx[u],add);
		u=fa[ltp[u]];
	}
	if(dep[u]>dep[v])swap(u,v);
	Modify(1,idx[u],idx[v],add);
}
int Opt2(int u,int v){
	int res=0;
	while(ltp[u]!=ltp[v]){
		if(dep[ltp[u]]<dep[ltp[v]])swap(u,v);
		res+=Query(1,idx[ltp[u]],idx[u]);
		res%=p,u=fa[ltp[u]];
	}
	if(dep[u]>dep[v])swap(u,v);
	return (res+Query(1,idx[u],idx[v]))%p;
}
void Opt3(int u,int add){
	add%=p;
	Modify(1,idx[u],idx[u]+siz[u]-1,add);
}
int Opt4(int u){
	return Query(1,idx[u],idx[u]+siz[u]-1)%p;
}
signed main(){
	Read(n),Read(m),Read(r),Read(p);
	for(rg int i=1;i<=n;i++)Read(ipt[i]);
	for(rg int i=1;i<n;i++){
		int u,v;
		Read(u),Read(v);
		ade(u,v),ade(v,u);
	}
	DFS1(r,0);
	DFS2(r,r);
	Build(1,1,n);
	for(rg int i=1;i<=m;i++){
		int opt,x,y,z;
		Read(opt),Read(x);
		switch(opt){
			case 1:{
				Read(y),Read(z);
				Opt1(x,y,z);
				break;
			}
			case 2:{
				Read(y);
				cout<<Opt2(x,y)%p<<endl;
				break;
			}
			case 3:{
				Read(z);
				Opt3(x,z);
				break;
			}
			case 4:{
				cout<<Opt4(x)%p<<endl;
				break;
			}
		}
	}
	return 0;
}

#define int LL
缺省源又臭又长放到其他blog里了。

3.总结

其实树剖听起来很毒瘤然而理解了就很简单了。
她可以解决许多想不出正解/十分毒瘤的树上问题。
树剖的代码看着长其实是数据结构长,树剖的核心代码并不长,而且很容易理解。

4.完结撒花

原文地址:https://www.cnblogs.com/juruoajh/p/13164227.html