相当于把树上的链抽出来变成区间进行操作。

重链剖分(本文重点):

本文依照P3384 【模板】轻重链剖分/树链剖分的实现进行讲解。

就是剖分出重链覆盖全树。

重链:除开头的端点为轻儿子外,其他节点全为重儿子的链。

重儿子:一个节点的儿子中子树大小最大的儿子。

要进行树链剖分,先要进行预处理。需要处理出以下信息:

  • $top[u]$ u节点所在重链的开头端点的编号。
  • $dep[u]$ u节点的深度。
  • $fa[u]$ u节点的父亲节点的编号。
  • $dfn[u]$ 时间戳,u节点被遍历到的顺序。
  • $rank[x]$ 时间戳为x的节点的编号。
  • $size[u]$ 以u为根的子树大小。
  • $son[u]$ u节点的重儿子的编号。

总共七个(七国之乱

预处理我们分为两个 dfs 来做。因为在重链剖分中优先遍历重儿子(为了使一条重链上的 $dfn$ 连续)

第一次 dfs 处理除 $top$、$dfn$ 和 $rank$ 外的基本信息把重链基本的剖分出来。

void dfs1(int u,int f)
{
	fa[u]=f;
	size[u]=1;
	int tmp=-1;
	for(int i=head[u];~i;i=nxt[i])
	{
		if(to[i]==f) continue;
		dep[to[i]]=dep[u]+1;
		dfs1(to[i],u);
		size[u]+=size[to[i]];
		if(size[to[i]]>tmp)
		{
			tmp=size[to[i]];
			son[u]=to[i];
		}
	}
}

第二次就主要为了把节点信息标完整。

void dfs2(int u,int tp)
{
	dfn[u]=++idx;
	rk[idx]=u;
	top[u]=tp;
	if(!son[u]) return;
	dfs2(son[u],tp);
	for(int i=head[u];~i;i=nxt[i])
	{
		if(to[i]==son[u]||to[i]==fa[u]) continue;
		dfs2(to[i],to[i]); 
	}
} 

然后考虑用线段树维护被用 $dfn$ 从原树中提取出来的区间。

线段树写法没有特殊,按照普通的线段树写即可。

唯一需要注意的是线段树的建树(见注释处)

void build(int i,int l,int r)
{
	t[i].l=l;t[i].r=r;t[i].mid=(l+r)>>1;
	t[i].d=t[i].add=0;
	if(l==r)
	{
		t[i].d=a[rk[l]]; //因为下标l,r是dfn
		return;
	}
	build(i<<1,l,t[i].mid);
	build(i<<1|1,t[i].mid+1,r);
	t[i].d=(t[i<<1].d+t[i<<1|1].d)%p;
}

其他部分和普通线段树是一样的。

然后逐一分析题目中的4个操作。

  • 1 x y z,表示将树从 x 到 y 结点最短路径上所有节点的值都加上 z。

  • 2 x y,表示求树从 x 到 y 结点最短路径上所有节点的值之和。

  • 3 x z,表示将以 x 为根节点的子树内所有节点值都加上 z。

  • 4 x,表示求以 x 为根节点的子树内所有节点值之和。

操作3,4非常好实现:

void mtree(int x,LL z)
{
	z%=p;
	t.modify_add(1,dfn[x],dfn[x]+size[x]-1,z);
}
LL qtree(int x)
{
	return t.query(1,dfn[x],dfn[x]+size[x]-1)%p;
}

因为区间下标是 $dfn$ 一个节点与其子树构成的区间是连续的。

再看操作1,2。我们分情况讨论,对于在同一重链上的两个点,我们直接对它们的 $dfn$ 区间进行操作即可。

对于不在同一重链上的两点,我们选择它们中所在重链的开头端点深度更深的(这样的目的是为了防止影响到无关的节点),对于它到其链顶进行区间操作,然后跳到它链顶的父亲,超级加辈,重复执行一直到两点在同一条重链上。

实现如下:

void mchain(int x,int y,LL z)
{
	z%=p;
	while(top[x]!=top[y])
	{
		if(dep[top[x]]<dep[top[y]]) Swap(x,y);
		t.modify_add(1,dfn[top[x]],dfn[x],z);
		x=fa[top[x]];
	}
	if(dfn[x]>dfn[y]) Swap(x,y); 
	t.modify_add(1,dfn[x],dfn[y],z);
}
LL qchain(int x,int y)
{
	LL res=0;
	while(top[x]!=top[y])
	{
		if(dep[top[x]]<dep[top[y]]) Swap(x,y);
		res=(res+t.query(1,dfn[top[x]],dfn[x]))%p;
		x=fa[top[x]];
	}
	if(dfn[x]>dfn[y]) Swap(x,y);
	res=(res+t.query(1,dfn[x],dfn[y]))%p;
	return res;
}

然后具体套用函数就好了。

对于维护边权的情况,我们只需要在 dfs1 中维护 a 数组,把该点的点权赋值为与父亲相连的边的边权就行了,本质上是边权转成点权。但是记得为根节点赋值。还有一个需要注意的就是在查询和修改链的时候,在同一条重链上的情况,我们在选择的区间应该是$[dfn[x]+1,dfn[y]]$,这样做可以防止 x 节点到其父节点的边权被算入答案。其他部分与普通树剖一致。

然后关于树链剖分求LCA