树链剖分之重链剖分

树链剖分

将树转化为线性序列,便于维护树上信息。

include: 重链剖分,长链剖分,实链剖分。

注:此文默认读者已经熟悉线段树的基本操作,不熟悉这可以先看这个:线段树

重链剖分

剖树

  1. 建图

    很普通的邻接表存图:

void ins(int x,int y)
{
	nex[++cnt]=fir[x];
	poi[cnt]=y;
	fir[x]=cnt;
}
  1. 通过第一遍 dfs,统计出每个节点的:

    • 父节点 fa

    • 深度 dep

    • 以当前节点为根的子树大小 siz

    • 当前节点所有子节点对应的子树中 siz 最大的节点,即重儿子 son

int fa[inf],son[inf],siz[inf],dep[inf];
void dfs1(int now,int from)
{
	fa[now]=from;siz[now]=1;
	dep[now]=dep[from]+1;
	for(int i=fir[now];i;i=nex[i])
	{
		int p=poi[i];
		if(p==from)continue;
		dfs1(p,now);
		siz[now]+=siz[p];
		if(siz[son[now]]<siz[p])
			son[now]=p;
	}
}
  1. 然后用第二遍 dfs,统计出:

    • 每条链的顶点 top

    • 节点的时间戳 dfn

    • 时间戳所对应的节点 rnk,即 rnk[dfn[x]]=x

    第二次 dfs 中,在确保深度优先的前提下,以重儿子优先搜索,以确保每条重链的时间戳是连续的。

int top[inf],dfn[inf],rnk[inf],sum;
void dfs2(int now,int topn)
{
	top[now]=topn;
	dfn[now]=++sum;rnk[sum]=now;
	if(!son[now])return;
	dfs2(son[now],topn);
	for(int i=fir[now];i;i=nex[i])
	{
		int p=poi[i];
		if(dfn[p])continue;
		dfs2(p,p);
	}
}
  1. 效果展示

经过两次 dfs,就将一棵树上的点按照 dfn 映射到了一个线性的序列上了。

如下边这棵树:

两次 dfs 之后这个树就是:

点中的数表示 dfs 序;红色的边表示重链,黑色的边表示轻链。

映射到线性序列,然后就可以用各种各样的数据结构(如线段树,分块,珂朵莉树,甚至直接暴力)来维护这个序列了。

luogu 模板

题目中包括 4 个操作:

  • 1 x y z 表示将树上从 \(x\)\(y\) 节点的最短路径上的节点权值都加上 \(z\)

  • 2 x y 表示查询树上从 \(x\)\(y\) 节点的最短路径上的节点权值之和。

  • 3 x z 表示将树上以 \(x\) 为根的子树上所有节点的权值都加上 \(z\)

  • 4 x 表示查询树上以 \(x\) 为根的子树上所有节点的权值之和。

可以分为两种:维护子树和维护链。

  1. 维护子树

观察上图,可以发现,同一子树上的点的 dfn 是连续的。也就是说,同一子树上的点在序列上也是连续的。举个例子:以 11 为根的子树的 siz 是 4,子树对应的 dfn 是从 11 到 14(即 11+4-1)。那么便可以直接进行区间维护。

void update(int i,int l,int r,int k)
{
	if(l<=T[i].le&&T[i].ri<=r)
	{
		T[i].val+=(T[i].ri-T[i].le+1)*k;T[i].val%=mod;
		T[i].tag+=k;T[i].tag%=mod;
		return;
	}
	if(T[i].tag)pushdown(i);
	int mid=(T[i].le+T[i].ri)>>1;
	if(l<=mid)update(i<<1,l,r,k);
	if(mid<r)update(i<<1|1,l,r,k);
	T[i].val=(T[i<<1].val+T[i<<1|1].val)%mod;
}
int ask(int i,int l,int r)
{
	if(l<=T[i].le&&T[i].ri<=r)
		return T[i].val%mod;
	if(T[i].tag)pushdown(i);
	int mid=(T[i].le+T[i].ri)>>1,ans=0;
	if(l<=mid)ans+=ask(i<<1,l,r),ans%=mod;
	if(mid<r)ans+=ask(i<<1|1,l,r),ans%=mod;
	return ans%mod;
}
  1. 维护链

在树上两点的简单路径必然经过 lca,而重链的时间戳是连续的。

那么可以在跳 lca 的过程中维护经过的重链。

void chain_add(int x,int y,int k)
{
	while(top[x]!=top[y])
	{
		if(dep[top[x]]<dep[top[y]])x^=y^=x^=y;
		update(1,dfn[top[x]],dfn[x],k);
		x=fa[top[x]];
	}
	if(dep[x]>dep[y])x^=y^=x^=y;
	update(1,dfn[x],dfn[y],k);
}
int chain_ask(int x,int y)
{
	int ans=0;
	while(top[x]!=top[y])
	{
		if(dep[top[x]]<dep[top[y]])x^=y^=x^=y;
		ans+=ask(1,dfn[top[x]],dfn[x]);ans%=mod;
		x=fa[top[x]];
	}
	if(dep[x]>dep[y])x^=y^=x^=y;
	ans+=ask(1,dfn[x],dfn[y]);
	return ans%mod;
}

完整代码

cosnt int inf=1e5+7;
struct Seg_Tree{
	int le,ri;
	int tag,val;
}T[inf<<2];
int n,m,root,mod,op,x,y,k,a[inf];
int fir[inf],nex[inf<<1],poi[inf<<1],cnt;
void ins(int x,int y)
{
	nex[++cnt]=fir[x];
	poi[cnt]=y;
	fir[x]=cnt;
}
int fa[inf],son[inf],siz[inf],dep[inf];
void dfs1(int now,int from)
{
	fa[now]=from;siz[now]=1;
	dep[now]=dep[from]+1;
	int maxn=0;
	for(int i=fir[now];i;i=nex[i])
	{
		int p=poi[i];
		if(p==from)continue;
		dfs1(p,now);
		siz[now]+=siz[p];
		if(siz[son[now]]<siz[p])
			son[now]=p;
	}
}
int top[inf],dfn[inf],rnk[inf],sum;
void dfs2(int now,int topn)
{
	top[now]=topn;
	dfn[now]=++sum;rnk[sum]=now;
	if(!son[now])return;
	dfs2(son[now],topn);
	for(int i=fir[now];i;i=nex[i])
	{
		int p=poi[i];
		if(dfn[p])continue;
		dfs2(p,p);
	}
}
void build(int i,int l,int r)
{
	T[i].le=l;T[i].ri=r;
	if(l==r)
	{
		T[i].val=a[rnk[l]]%mod;
		return;
	}
	int mid=(l+r)>>1;
	build(i<<1,l,mid);
	build(i<<1|1,mid+1,r);
	T[i].val=(T[i<<1].val+T[i<<1|1].val)%mod;
}
void pushdown(int i)
{
	T[i<<1].val+=(T[i<<1].ri-T[i<<1].le+1)*T[i].tag;
	T[i<<1|1].val+=(T[i<<1|1].ri-T[i<<1|1].le+1)*T[i].tag;
	T[i<<1].val%=mod;T[i<<1|1].val%=mod;
	T[i<<1].tag+=T[i].tag;T[i<<1].tag%=mod;
	T[i<<1|1].tag+=T[i].tag;T[i<<1|1].tag%=mod;
	T[i].tag=0;
}
void update(int i,int l,int r,int k)
{
	if(l<=T[i].le&&T[i].ri<=r)
	{
		T[i].val+=(T[i].ri-T[i].le+1)*k;T[i].val%=mod;
		T[i].tag+=k;T[i].tag%=mod;
		return;
	}
	if(T[i].tag)pushdown(i);
	int mid=(T[i].le+T[i].ri)>>1;
	if(l<=mid)update(i<<1,l,r,k);
	if(mid<r)update(i<<1|1,l,r,k);
	T[i].val=(T[i<<1].val+T[i<<1|1].val)%mod;
}
int ask(int i,int l,int r)
{
	if(l<=T[i].le&&T[i].ri<=r)
		return T[i].val%mod;
	if(T[i].tag)pushdown(i);
	int mid=(T[i].le+T[i].ri)>>1,ans=0;
	if(l<=mid)ans+=ask(i<<1,l,r),ans%=mod;
	if(mid<r)ans+=ask(i<<1|1,l,r),ans%=mod;
	return ans%mod;
}
void chain_add(int x,int y,int k)
{
	while(top[x]!=top[y])
	{
		if(dep[top[x]]<dep[top[y]])x^=y^=x^=y;
		update(1,dfn[top[x]],dfn[x],k);
		x=fa[top[x]];
	}
	if(dep[x]>dep[y])x^=y^=x^=y;
	update(1,dfn[x],dfn[y],k);
}
int chain_ask(int x,int y)
{
	int ans=0;
	while(top[x]!=top[y])
	{
		if(dep[top[x]]<dep[top[y]])x^=y^=x^=y;
		ans+=ask(1,dfn[top[x]],dfn[x]);ans%=mod;
		x=fa[top[x]];
	}
	if(dep[x]>dep[y])x^=y^=x^=y;
	ans+=ask(1,dfn[x],dfn[y]);
	return ans%mod;
}
signed main()
{
	n=re();m=re();root=re();mod=re();
	for(int i=1;i<=n;i++)
		a[i]=re();
	for(int i=1;i<n;i++)
	{
		int u=re(),v=re();
		ins(u,v);ins(v,u);
	}
	dfs1(root,root);
	dfs2(root,root);
	build(1,1,n);
	for(int i=1;i<=m;i++)
	{
		op=re();x=re();
		if(op==1)y=re(),k=re(),chain_add(x,y,k%mod);
		if(op==2)y=re(),wr(chain_ask(x,y)%mod),putchar('\n');
		if(op==3)k=re(),update(1,dfn[x],dfn[x]+siz[x]-1,k%mod);
		if(op==4)wr(ask(1,dfn[x],dfn[x]+siz[x]-1)%mod),putchar('\n');
	}
	return 0;
}

时间复杂度

树剖有两条性质:

  1. 如果 \((x,y)\) 是轻链,那么必有 \(2\cdot siz_y<siz_x\)
  2. 根节点到任意节点的路径上的轻重链个数都少于 \(\log n\)

综上,树剖的时间复杂度就是 \(O(n\log n)\) 的。

再套上线段树,时间复杂度为 \(O(n\log^2n)\)

当然,树剖不一定要结合线段树,或者说不一定要结合数据结构。因为树剖之后有一些美妙的性质,可以以此维护一些树上信息。

例题选讲

树剖维护树上信息

第一篇题解我写的

树剖珂朵莉树

话说我珂朵莉比分块学得早

题意转化为树剖问题就是:

  1. 子树推平为 0
  2. 链上推平为 1

区间推平,那必然是珂朵莉啊。

不过,这个题不开 O2 会 T 掉,不知道当年考场上开了吗。

const int inf=1e5+7;
int n,m,x;
struct Chtholly{
	int l,r;
	mutable int val;
	Chtholly(int l,int r,int val):
		l(l),r(r),val(val){}
	bool operator <(const Chtholly &b)const
	{
		return l<b.l;
	}
};
set<Chtholly>tre;
#define IT set<Chtholly>::iterator
int fir[inf],nex[inf],poi[inf],cnt;
void ins(int x,int y)
{
	nex[++cnt]=fir[x];
	poi[cnt]=y;
	fir[x]=cnt;
}
int fa[inf],dep[inf],siz[inf],son[inf];
void dfs1(int now,int from)
{
	siz[now]=1;fa[now]=from;
	dep[now]=dep[from]+1;
	for(int i=fir[now];i;i=nex[i])
	{
		int p=poi[i];
		if(p==from)continue;
		dfs1(p,now);
		siz[now]+=siz[p];
		if(siz[son[now]]<siz[p])
			son[now]=p;
	}
}
int top[inf],dfn[inf],rnk[inf],sum;
void dfs2(int now,int topn)
{
	top[now]=topn;
	dfn[now]=++sum;rnk[sum]=now;
	if(son[now]==0)return;
	dfs2(son[now],topn);
	for(int i=fir[now];i;i=nex[i])
	{
		int p=poi[i];
		if(dfn[p])continue;
		dfs2(p,p);
	}
}
IT split(int now)
{
	IT i=tre.lower_bound(Chtholly(now,0,0));
	if(i!=tre.end()&&i->l==now)return i;
	i--;int l=i->l,r=i->r,v=i->val;
	tre.erase(i);
	tre.insert(Chtholly(l,now-1,v));
	return tre.insert(Chtholly(now,r,v)).first; 
}
int assign(int l,int r,int k)
{
	IT ir=split(r+1),il=split(l),i=il;
	int ans=0;
	while(i!=ir)
	{
		if(i->val!=k)ans+=i->r-i->l+1;
		i++;
	}
	tre.erase(il,ir);
	tre.insert(Chtholly(l,r,k));
	return ans;
}
int cover(int l,int r,int k)
{
	int ans=0;
	while(top[l]!=top[r])
	{
		ans+=assign(dfn[top[r]],dfn[r],k);
		r=fa[top[r]];
	}
	ans+=assign(dfn[l],dfn[r],k);
	return ans;
}
int main()
{
	n=re();
	for(int i=1;i<n;i++)
		ins(re()+1,i+1);
	dfs1(1,1);dfs2(1,1);
	tre.insert(Chtholly(1,n,0));
	m=re();
	for(int i=1;i<=m;i++)
	{
		char s[10]="";
		scanf("%s",s);x=re()+1;
		if(s[0]=='i')wr(cover(1,x,1)),putchar('\n');
		else wr(assign(dfn[x],dfn[x]+siz[x]-1,0)),putchar('\n');
	}
	return 0;
}

树剖分块

每次单点修改后暴力维护块内的 sum 和 maxn,然后树剖查询。

时间复杂度 \(O(n\sqrt n\log n)\),跑到了最优解第 7

const int inf=1e5+7;
int n,m,len,a[inf],c[inf];
int fir[inf],nex[inf<<1],poi[inf<<1],cnt;
void ins(int x,int y)
{
	nex[++cnt]=fir[x];
	poi[cnt]=y;
	fir[x]=cnt;
}
int fa[inf],son[inf],siz[inf],dep[inf];
void dfs1(int now,int from)
{
	dep[now]=dep[from]+1;
	siz[now]=1;fa[now]=from;
	for(int i=fir[now];i;i=nex[i])
	{
		int p=poi[i];
		if(p==from)continue;
		dfs1(p,now);
		siz[now]+=siz[p];
		if(siz[son[now]]<siz[p])
			son[now]=p;
	}
}
int top[inf],dfn[inf],val[inf],col[inf],dfn_;
void dfs2(int now,int topn)
{
	top[now]=topn;dfn[now]=++dfn_;
	val[dfn_]=a[now];col[dfn_]=c[now];
	if(!son[now])return;
	dfs2(son[now],topn);
	for(int i=fir[now];i;i=nex[i])
	{
		int p=poi[i];
		if(dfn[p])continue;
		dfs2(p,p);
	}
}
int bel[inf],L[400],R[400];
int sum[inf][400],maxn[inf][400];
int query_sum(int l,int r,int c)
{
	int lin=bel[l],rin=bel[r];
	int ans=0;
	if(lin==rin)
	{
		for(int i=l;i<=r;i++)
			if(col[i]==c)ans+=val[i];
		return ans;
	}
	for(int i=l;i<=R[lin];i++)
		if(col[i]==c)ans+=val[i];
	for(int i=L[rin];i<=r;i++)
		if(col[i]==c)ans+=val[i];
	for(int i=lin+1;i<rin;i++)
		ans+=sum[c][i];
	return ans;
}
int ask_sum(int x,int y,int c)
{
	int ans=0;
	while(top[x]^top[y])
	{
		if(dep[top[x]]<dep[top[y]])swap(x,y);
		ans+=query_sum(dfn[top[x]],dfn[x],c);
		x=fa[top[x]];
	}
	if(dep[x]>dep[y])swap(x,y);
	return ans+query_sum(dfn[x],dfn[y],c);
}
int query_max(int l,int r,int c)
{
	int lin=bel[l],rin=bel[r];
	int ans=0;
	if(lin==rin)
	{
		for(int i=l;i<=r;i++)
			if(col[i]==c)ans=max(ans,val[i]);
		return ans;
	}
	for(int i=l;i<=R[lin];i++)
		if(col[i]==c)ans=max(ans,val[i]);
	for(int i=L[rin];i<=r;i++)
		if(col[i]==c)ans=max(ans,val[i]);
	for(int i=lin+1;i<rin;i++)
		ans=max(ans,maxn[c][i]);
	return ans;
}
int ask_max(int x,int y,int c)
{
	int ans=0;
	while(top[x]^top[y])
	{
		if(dep[top[x]]<dep[top[y]])swap(x,y);
		ans=max(ans,query_max(dfn[top[x]],dfn[x],c));
		x=fa[top[x]];
	}
	if(dep[x]>dep[y])swap(x,y);
	return max(ans,query_max(dfn[x],dfn[y],c));
}
int main()
{
	n=re();m=re();len=sqrt(n);
	for(int i=1;i<=n;i++)
		a[i]=re(),c[i]=re();
	for(int i=1;i<n;i++)
	{
		int u=re(),v=re();
		ins(u,v),ins(v,u);
	}
	dfs1(1,1),dfs2(1,1);
	for(int i=1;i<=len;i++)
		L[i]=R[i-1]+1,R[i]=i*len;
	R[len]=n;
	for(int i=1;i<=len;i++)
	{
		for(int j=L[i];j<=R[i];j++)
		{
			bel[j]=i,sum[col[j]][i]+=val[j];
			maxn[col[j]][i]=max(maxn[col[j]][i],val[j]);
		}
	}
	for(int i=1;i<=m;i++)
	{
		char op[10]="";scanf("%s",op);
		int x=re(),y=re();
		if(op[1]=='C')
		{
			x=dfn[x];
			for(int i=L[bel[x]];i<=R[bel[x]];i++)
				sum[col[i]][bel[x]]=maxn[col[i]][bel[x]]=0;
			col[x]=y;
			for(int i=L[bel[x]];i<=R[bel[x]];i++)
			{
				sum[col[i]][bel[x]]+=val[i];
				maxn[col[i]][bel[x]]=max(maxn[col[i]][bel[x]],val[i]);
			}
		}
		if(op[1]=='W')
		{
			x=dfn[x];
			for(int i=L[bel[x]];i<=R[bel[x]];i++)
				sum[col[i]][bel[x]]=maxn[col[i]][bel[x]]=0;
			val[x]=y;
			for(int i=L[bel[x]];i<=R[bel[x]];i++)
			{
				sum[col[i]][bel[x]]+=val[i];
				maxn[col[i]][bel[x]]=max(maxn[col[i]][bel[x]],val[i]);
			}
		}
		if(op[1]=='S')wr(ask_sum(x,y,col[dfn[x]])),putchar('\n');
		if(op[1]=='M')wr(ask_max(x,y,col[dfn[x]])),putchar('\n');
	}
	return 0;
}

练习

换根树剖

先看一道 例题

操作 1 就是换根,而 2 和 3 是树剖的常规操作:链上更新,子树查询。

第一次看到换根树剖的时候,首先想到的思路是将这个树再剖一遍。但显然时间复杂度不允许。

在学树剖时,我们就将子树操作和链上操作分开讨论了。在这里,我们继续传承优良传统。

链上操作

可以发现,链上更新其实和根的位置没有关系。所以链上推平就可以直接用线段树来解决。

void pushdown(int i)
{
	T[i<<1].minn=T[i<<1|1].minn=T[i].tag;
	T[i<<1].tag=T[i<<1|1].tag=T[i].tag;
	T[i].tag=0;
}
void assign(int i,int l,int r,int k)
{
	if(l<=T[i].le&&T[i].ri<=r)
	{
		T[i].minn=k;
		T[i].tag=k;
		return;
	}
	if(T[i].tag)pushdown(i);
	int mid=(T[i].le+T[i].ri)>>1;
	if(l<=mid)assign(i<<1,l,r,k);
	if(mid<r)assign(i<<1|1,l,r,k);
	T[i].minn=min(T[i<<1].minn,T[i<<1|1].minn);
}
void swap(int &x,int &y){x^=y^=x^=y;}
void update(int x,int y,int k)
{
	while(top[x]!=top[y])
	{
		if(dep[top[x]]<dep[top[y]])swap(x,y);
		assign(1,dfn[top[x]],dfn[x],k);
		x=fa[top[x]];
	}
	if(dep[x]>dep[y])swap(x,y);
	assign(1,dfn[x],dfn[y],k);
}

子树操作

但子树查询不同。根的位置不同导致两个节点的 lca 不同,子树更不相同。

那怎么办?

我们可以 破罐子破摔 以 1 为根,先剖一遍。

然后记录下来每次要换成的根 root。

对于要查询的点 x,与 root 存在四种位置关系:

  1. x 就是 root
  2. root 是 x 的祖先
  3. x 和 root 无直接血缘
  4. x 是 root 的祖先

显然,第 1 种情况以 x 为根的子树就是整棵树,所以直接输出全局 min 值。

那么我们试着分析一下这三种情况中以 x 为根的子树长什么样。

1. root 是 x 的祖先

(图丑,凑活看吧)

显然,根为 1 时的 x 子树和根为 root 时的 x 子树没有什么区别,所以直接查询就可以了。

2. root 和 x 无直接血缘

由图可知,此情况和情况 1 相同。

3. x 是 root 的祖先

这种情况就是换根树剖真正需要讨论的了。

由图,我们先把 root 拉上去:

……蜈蚣……

和原图做对比,会发现,除了原图中 x 子树包含 root 的子节点,其余节点均属于新图中以 x 的子树。

那么现在的问题就在于,如何找到这个子树包含 root 的子节点。

暴力跳呗

while(fa[root]!=x)root=fa[root];

确实可以,但……你不是把树剖了吗?

那我们往上跳重链,它不香吗?

如果重链顶点的父亲是 x,就返回重链顶点。

如果 x 已经包含在了重链中,那就返回 x 的重儿子。

否则就向上跳重链。

int find(int x)
{
	int now=root;
	while(top[now]!=top[x])
	{
		if(fa[top[now]]==x)return top[now];
		now=fa[top[now]];
	}
	return son[x];
}

那么不查询的是一个子树,时间戳是连续的,查询的就可以拆成两部分:

int ls=find(x);
wr(min(ask(1,1,dfn[ls]-1),ask(1,dfn[ls]+siz[ls],n)));

Code

const int inf=1e5+7;
int n,m,root;
int a[inf];
int fir[inf],nex[inf<<1],poi[inf<<1],cnt;
void ins(int x,int y)
{
	nex[++cnt]=fir[x];
	poi[cnt]=y;
	fir[x]=cnt;
}
int fa[inf],son[inf],dep[inf],siz[inf];
void dfs1(int now,int from)
{
	fa[now]=from;siz[now]=1;
	dep[now]=dep[from]+1;
	for(int i=fir[now];i;i=nex[i])
	{
		int p=poi[i];
		if(p==from)continue;
		dfs1(p,now);
		siz[now]+=siz[p];
		if(siz[p]>siz[son[now]])
			son[now]=p;
	}
}
int top[inf],dfn[inf],rnk[inf],sum;
void dfs2(int now,int topn)
{
	top[now]=topn;
	dfn[now]=++sum;rnk[sum]=now;
	if(!son[now])return;
	dfs2(son[now],topn);
	for(int i=fir[now];i;i=nex[i])
	{
		int p=poi[i];
		if(dfn[p])continue;
		dfs2(p,p);
	}
}
struct Seg_Tree{
	int le,ri;
	int tag,minn;
}T[inf<<2];
int min(int a,int b){return a<b?a:b;}
void build(int i,int l,int r)
{
	T[i].le=l;T[i].ri=r;
	if(l==r)
	{
		T[i].minn=a[rnk[l]];
		return;
	}
	int mid=(l+r)>>1;
	build(i<<1,l,mid);
	build(i<<1|1,mid+1,r);
	T[i].minn=min(T[i<<1].minn,T[i<<1|1].minn);
}
void pushdown(int i)
{
	T[i<<1].minn=T[i<<1|1].minn=T[i].tag;
	T[i<<1].tag=T[i<<1|1].tag=T[i].tag;
	T[i].tag=0;
}
void assign(int i,int l,int r,int k)
{
	if(l<=T[i].le&&T[i].ri<=r)
	{
		T[i].minn=k;
		T[i].tag=k;
		return;
	}
	if(T[i].tag)pushdown(i);
	int mid=(T[i].le+T[i].ri)>>1;
	if(l<=mid)assign(i<<1,l,r,k);
	if(mid<r)assign(i<<1|1,l,r,k);
	T[i].minn=min(T[i<<1].minn,T[i<<1|1].minn);
}
void swap(int &x,int &y){x^=y^=x^=y;}
void update(int x,int y,int k)
{
	while(top[x]!=top[y])
	{
		if(dep[top[x]]<dep[top[y]])swap(x,y);
		assign(1,dfn[top[x]],dfn[x],k);
		x=fa[top[x]];
	}
	if(dep[x]>dep[y])swap(x,y);
	assign(1,dfn[x],dfn[y],k);
}
int lca_(int x,int y)
{
	while(top[x]!=top[y])
	{
		if(dep[top[x]]<dep[top[y]])swap(x,y);
		x=fa[top[x]];
	}
	if(dep[x]>dep[y])swap(x,y);
	return x;
}
int ask(int i,int l,int r)
{
	if(l<=T[i].le&&T[i].ri<=r)
		return T[i].minn;
	if(T[i].tag)pushdown(i);
	int mid=(T[i].le+T[i].ri)>>1,ans=0x7fffffff;
	if(l<=mid)ans=min(ans,ask(i<<1,l,r));
	if(mid<r)ans=min(ans,ask(i<<1|1,l,r));
	return ans;
}
int find(int x)
{
	int now=root;
	while(top[now]!=top[x])
	{
		if(fa[top[now]]==x)return top[now];
		now=fa[top[now]];
	}
	return son[x];
}
int main()
{
	n=re();m=re();
	for(int i=1;i<n;i++)
	{
		int u=re(),v=re();
		ins(u,v),ins(v,u);
	}
	for(int i=1;i<=n;i++)
		a[i]=re();
	dfs1(1,1);dfs2(1,1);
	root=re();
	build(1,1,n);
	for(int i=1;i<=m;i++)
	{
		int op=re();
		if(op==1)root=re();
		if(op==2)
		{
			int x=re(),y=re(),k=re();
			update(x,y,k);
		}
		if(op==3)
		{
			int x=re(),lca=lca_(x,root);
			if(x==root)wr(T[1].minn),putchar('\n');
			else if(lca==x)
			{
				int ls=find(x);
				wr(min(ask(1,1,dfn[ls]-1),ask(1,dfn[ls]+siz[ls],n)));
				putchar('\n');
			}
			else
			{
				wr(ask(1,dfn[x],dfn[x]+siz[x]-1));
				putchar('\n');
			}
		}
	}
	return 0;
}

练习

长链剖分

听说是用来优化 DP 的,鉴于本人 DP 很烂,没有学。

我才不会承认是因为我没学会。。。

实链剖分

posted @ 2022-02-09 15:03  Zvelig1205  阅读(201)  评论(0编辑  收藏  举报