关于树链剖分

13 篇文章 1 订阅
5 篇文章 0 订阅
本文详细介绍了树链剖分的概念和实现,通过构建树并统计子树大小确定重儿子,进而进行重轻链的划分。使用线段树维护重链,实现快速查询和修改操作,达到logn的时间复杂度。文章通过实例展示了如何进行链的映射、线段树的构造以及查询、修改操作,同时讲解了lca在更新过程中的应用,为解决树上动态查询问题提供了一种高效方法。
摘要由CSDN通过智能技术生成

树链剖分

模板题

顾名思义,即是把树的形态用线性链表表示出来。
那么首先就是要造个树。

ll siz[N],dep[N],son[N],fa[N];//son是以x为祖先的重儿子坐标 
void build_tree(int now)
{
	siz[now]=1;//初始化树的大小
	for(int i=he[now];i;i=nxt[i])
	{
		int tar=to[i];
		if(tar==fa[now]) continue;
		fa[tar]=now,dep[tar]=dep[now]+1;
		build_tree(tar);
		siz[now]+=siz[tar];
		if(siz[tar]>siz[son[now]]) son[now]=tar;
	 } 
}

那么首先在这里就是除了正常造树,还要顺便统计一下以每个节点为根的子树大小,以方便后面重儿子,轻儿子的确定,那么什么是重儿子呢,比如我现在有个2号节点,那么我有4,5两个儿子节点,那么以4,5为节点的分别的子树大小为3,2那么因为以4的节点子树较大,那么我们说4是3的重儿子,连着一条重边,代码中的son数组就是记录以当前节点为父节点的重儿子的id。

那么接下来就是要用数组通过刚才对重儿子的标记来进行分链的操作。

ll past[N]/*关于dfs序对点的坐标的一个映射*/,dfs[N]/*所在点的dfs序*/,top[N],tot,ctr[N]; 
void remark(int poi,int tp)//所在点以及当前点所在重链的顶端 
{
    top[poi]=tp;
 	dfs[poi]=++tot;
 	past[tot]=poi;
 	if(son[poi]) remark(son[poi],tp);//优先往重链那里走
	for(int i=he[poi];i;i=nxt[i])
	{
		int tar=to[i];
		if(tar!=son[poi]&&tar!=fa[poi]) remark(tar,tar);//轻链
	 } 
 	ctr[poi]=tot;//子树的范围是dfs[poi]~ctr[poi],记录的是以poi为根的子树的最大dfs序 
}

这里的那个ctr数组记录的是每个子树的最大dfs续,其实也就是每个链条划分的长度。

那么,为了方便查询这些链条我们需要用一些其他的数据结构来维护这些链条。这里小编用的是线段树。

int len;//对于线段树的dfs序记录, 
struct node
{
	int l,r,idl,idr/*关于len的映射 左右儿子的编号*/;
	ll c,late;//c记录重链权值 
}tree[N];
void line_tree(int x,int y)
{
	++len;
	tree[len].l=x;
	tree[len].r=y;
	tree[len].late=0;
	if(x==y)
	{
	  tree[len].idl=tree[len].idr=-1;
	  tree[len].c=val[past[x]];//映射回原来主树的编号	
	}		
	else
	{
		int now=len,mid=(x+y)>>1;
		tree[now].idl=len+1,line_tree(x,mid);
		tree[now].idr=len+1,line_tree(mid+1,y);
		tree[now].c=tree[tree[now].idl].c+tree[tree[now].idr].c;
	} 
}
void sent(int x)//线段树的下传操作,late部分为懒标记 
{	
	if(tree[x].late)
	{
		tree[x].c+=tree[x].late*(tree[x].r-tree[x].l+1);
		int q=tree[x].idl,p=tree[x].idr;
		if(q!=-1) tree[q].late+=tree[x].late,tree[p].late+=tree[x].late;//如果以此为节点后还有儿子那么进行下传操作 
		tree[x].late=0;
	}	
} 

这里我们用到结构体来当树单元比较方便记录,这里实际上是通过树上每一个链条的范围来进行映射,这样子,通过重轻链的划分, 每次查询可以做到logn的时间复杂度。

那么实际上到这里这里就可以实现查询了,那么怎么去改变树里面的权值呢。

void change(int now,int x,int y,int z)
{
	if(tree[now].l==x&&tree[now].r==y)
	{ 
		tree[now].late+=z;
		sent(now);
		return; 
	}	
	sent(now);
	int q=tree[now].idl,p=tree[now].idr;
	int mid=(tree[now].l+tree[now].r)>>1;
	if(y<=mid) change(q,x,y,z);
	else if(x>mid) change(p,x,y,z);
	else change(q,x,mid,z),change(p,mid+1,y,z);
	tree[now].c+=(y-x+1)*z;
} 

这里实际上就是对链的长度进行修改便于权值下传,并且修改区域里面的和值。

接下来我们就要进行查询。

void lca(int x,int y,int z)
{
	while(top[x]!=top[y])//两个点不在一条重链上 
	{
		if(dep[top[x]]>dep[top[y]]) swap(x,y);//深度深的默认为y优先跳重链顶点深的
		change(1,dfs[top[y]],dfs[y],z);//从第一层开始找起 
		y=fa[top[y]]; 
	}	
	if(dep[x]>dep[y]) swap(x,y);//交换 
	change(1,dfs[x],dfs[y],z);//当在同一条重链再更新一次 
} 

这里用到lca,这里因为给的是原树的id那么我们就要用映射的线段树的id,所以要用dfs来从线段树第一层开始找起。

void sum(int x,int y)//统计x~y的权值和
{
	ll ans=0;
	while(top[x]!=top[y])
	{
		if(dep[top[x]]>dep[top[y]]) swap(x,y);
		ans=(ans+get_sum(1,dfs[top[y]],dfs[y])%p)%p;
		y=fa[top[y]];	
	}
	if(dep[x]>dep[y]) swap(x,y);
	ans+=get_sum(1,dfs[x],dfs[y]);//已经在同一条重链上 
	ks(ans%p),putchar('\n');	 
} 

完整代码

#include<cstdio>
#include<iostream>
using namespace std;
typedef long long ll;
const ll N=2e5+11;
ll n,m,r,p;
int opt,x,y,z;
ll cnt,val[N],to[N],he[N],nxt[N];
ll qread()
{
	ll xx=0;
	char c=getchar();
	while(c<'0'||c>'9') c=getchar();
	while(c>='0'&&c<='9') 
	{
		xx=(xx<<1)+(xx<<3)+c-'0';
		c=getchar();	
	}	
	return xx;
}
void ks(ll num)
{
    if(num>9) ks(num/10ll);
	putchar(num%10+'0');	
}
void add(int u,int v)
{
	++cnt;
	nxt[cnt]=he[u];
	he[u]=cnt;
	to[cnt]=v;
}
ll siz[N],dep[N],son[N],fa[N];//son是以x为祖先的重儿子坐标 
void build_tree(int now)
{
	siz[now]=1;//初始化树的大小
	for(int i=he[now];i;i=nxt[i])
	{
		int tar=to[i];
		if(tar==fa[now]) continue;
		fa[tar]=now,dep[tar]=dep[now]+1;
		build_tree(tar);
		siz[now]+=siz[tar];
		if(siz[tar]>siz[son[now]]) son[now]=tar;
	 } 
}
ll past[N]/*关于dfs序对点的坐标的一个映射*/,dfs[N]/*所在点的dfs序*/,top[N],tot,ctr[N]; 
void remark(int poi,int tp)//所在点以及当前点所在重链的顶端 
{
    top[poi]=tp;
 	dfs[poi]=++tot;
 	past[tot]=poi;
 	if(son[poi]) remark(son[poi],tp);//优先往重链那里走
	for(int i=he[poi];i;i=nxt[i])
	{
		int tar=to[i];
		if(tar!=son[poi]&&tar!=fa[poi]) remark(tar,tar);//轻链
	 } 
 	ctr[poi]=tot;//子树的范围是dfs[poi]~ctr[poi],记录的是以poi为根的子树的最大dfs序 
}
int len;//对于线段树的dfs序记录, 
struct node
{
	int l,r,idl,idr/*关于len的映射 左右儿子的编号*/;
	ll c,late;//c记录重链权值 
}tree[N];
void line_tree(int x,int y)
{
	++len;
	tree[len].l=x;
	tree[len].r=y;
	tree[len].late=0;
	if(x==y)
	{
	  tree[len].idl=tree[len].idr=-1;
	  tree[len].c=val[past[x]];//映射回原来主树的编号	
	}		
	else
	{
		int now=len,mid=(x+y)>>1;
		tree[now].idl=len+1,line_tree(x,mid);
		tree[now].idr=len+1,line_tree(mid+1,y);
		tree[now].c=tree[tree[now].idl].c+tree[tree[now].idr].c;
	} 
}
void sent(int x)//线段树的下传操作,late部分为懒标记 
{	
	if(tree[x].late)
	{
		tree[x].c+=tree[x].late*(tree[x].r-tree[x].l+1);
		int q=tree[x].idl,p=tree[x].idr;
		if(q!=-1) tree[q].late+=tree[x].late,tree[p].late+=tree[x].late;//如果以此为节点后还有儿子那么进行下传操作 
		tree[x].late=0;
	}	
} 
void change(int now,int x,int y,int z)
{
	if(tree[now].l==x&&tree[now].r==y)
	{ 
		tree[now].late+=z;
		sent(now);
		return; 
	}	
	sent(now);
	int q=tree[now].idl,p=tree[now].idr;
	int mid=tree[now].l+tree[now].r>>1;
	if(y<=mid) change(q,x,y,z);
	else if(x>mid) change(p,x,y,z);
	else change(q,x,mid,z),change(p,mid+1,y,z);
	tree[now].c+=(y-x+1)*z;
} 
ll get_sum(int now,int x,int y)
{
	sent(now);//访问时先进行下传操作,不然可能会导致数据损失
	if(tree[now].l==x&&tree[now].r==y) return tree[now].c;
	int q=tree[now].idl,p=tree[now].idr;
	int mid=(tree[now].l+tree[now].r)>>1;
	if(y<=mid) return get_sum(q,x,y);
	else if(x>mid) return get_sum(p,x,y);
	else return get_sum(q,x,mid)+get_sum(p,mid+1,y);
}
void lca(int x,int y,int z)
{
	while(top[x]!=top[y])//两个点不在一条重链上 
	{
		if(dep[top[x]]>dep[top[y]]) swap(x,y);//深度深的默认为y优先跳重链顶点深的
		change(1,dfs[top[y]],dfs[y],z);//从第一层开始找起 
		y=fa[top[y]]; 
	}	
	if(dep[x]>dep[y]) swap(x,y);//交换 
	change(1,dfs[x],dfs[y],z);//当在同一条重链再更新一次 
} 
void sum(int x,int y)//统计x~y的权值和
{
	ll ans=0;
	while(top[x]!=top[y])
	{
		if(dep[top[x]]>dep[top[y]]) swap(x,y);
		ans=(ans+get_sum(1,dfs[top[y]],dfs[y])%p)%p;
		y=fa[top[y]];	
	}
	if(dep[x]>dep[y]) swap(x,y);
	ans+=get_sum(1,dfs[x],dfs[y]);//已经在同一条重链上 
	ks(ans%p),putchar('\n');	 
} 
void get_sum_sontree(int x)
{
	ks(get_sum(1,dfs[x],ctr[x])%p),putchar('\n'); 
}
void change_sontree(int x,int y)
{
	change(1,dfs[x],ctr[x],y);
}
int main()
{
	n=qread(),m=qread(),r=qread(),p=qread();
	for(int i=1;i<=n;++i) val[i]=qread();
	for(int i=1,x,y;i<=n-1;++i) x=qread(),y=qread(),add(x,y),add(y,x);
    build_tree(r);//建主树 
    remark(r,r);//记录重链 
    line_tree(1,tot);//用线段树维护重链 
	while(m--)
	{
		opt=qread();
		if(opt==1)
		{
			x=qread(),y=qread(),z=qread();
			lca(x,y,z);	
		}
		else if(opt==2)
		{
			x=qread(),y=qread();
			sum(x,y);
		}
		else if(opt==3)
		{
			x=qread(),z=qread();
			change_sontree(x,z);
		}
		else
		{
			x=qread();
			get_sum_sontree(x);
		}
	}
	return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值