dsu on tree(树上启发式合并)详解

题外话

据说dsu on tree这个名字十分的离谱,原先它指的是一种代码风格,后来莫名其妙用来指代树上启发式合并这个算法。

以及这个算法和dsu(并查集)更是没有什么关系,这也十分的离谱qwq。

介绍

这是个用来处理这样一类题目的:询问支持离线,并且询问与子树有关。

它可以很方便地在 O ( n l o g n ) O(nlogn) O(nlogn) 的时间内完成。

正题

它是一个利用了重链剖分的一个性质的暴力。

先掏一道模板题来讲吧:

给出一棵节点有颜色的树,每次询问某个节点的子树内有多少种不同的颜色。

我们考虑离线做这个问题,先求出每个节点的答案,然后每次直接 O ( 1 ) O(1) O(1) 回答即可。

考虑对每一个节点求出一个数组,记录每种颜色的出现次数,从而维护出不同颜色的数量,这样做是 n 2 n^2 n2 的。

注意到一个节点的数组可以由儿子们的数组合并得到,由于儿子们求出答案后数组就用不到了,我们就可以直接拿一个儿子的数组来,然后将剩下的儿子的数组合并过来(指暴力合并,一个一个插入)。显然一开始拿的那个数组,拿重儿子的数组是最好的,这样可以减少合并次数。

然后就会惊人地发现,这个暴力时间复杂度竟是优美的 O ( n log ⁡ n ) O(n\log n) O(nlogn)

证明也很简单,利用轻重链剖分的性质,一个点到根路径上不超过 log ⁡ n \log n logn 条轻边,也就是说,一个点最多会被暴力合并 log ⁡ n \log n logn 次。

这个就是树上启发式合并,即每次将轻儿子的信息暴力合并到重儿子的信息上从而得到自己的信息,本质上和一般的启发式合并是一样的:将较小的块合并到较大的块,从而保证每个点至多被合并 log ⁡ n \log n logn 次,因为一个点每次合并后所在的块的大小至少翻倍。

然而实现的时候,我们不需要维护轻儿子的数组,一般我们选择全局只维护一个数组,记录上一个点的信息,然后每次暴力遍历轻儿子的子树将信息合并过来。我们只需要每次先处理了轻儿子,然后清空数组再遍历重儿子,就可以使上一个点的信息成为我们需要的重儿子的信息了。

关于遍历轻儿子子树这个部分,你也可以预处理出dfs序后改成遍历区间,这就是上面所提到的那个dsu on tree代码风格……

代码:

#include <cstdio>
#include <cstring>
#define maxn 100010

int n,m,col[maxn];
struct edge{int y,next;};
edge e[maxn*2];
int first[maxn];
void buildroad(int x,int y)
{
	static int len=0;
	e[++len]=(edge){y,first[x]};
	first[x]=len;
}
int size[maxn],mson[maxn];
void dfs1(int x,int fa)//求重儿子
{
	size[x]=1;
	for(int i=first[x];i;i=e[i].next)
	{
		int y=e[i].y;
		if(y==fa)continue;
		dfs1(y,x);
		if(size[y]>size[mson[x]])mson[x]=y;
		size[x]+=size[y];
	}
}
int tong[maxn],ans[maxn],now_ans=0;
void go(int x,int fa,int type)
{
	tong[col[x]]+=type;
	if(type==1&&tong[col[x]]==1)now_ans++;
	if(type==-1&&tong[col[x]]==0)now_ans--;
	for(int i=first[x];i;i=e[i].next)
	if(e[i].y!=fa)go(e[i].y,x,type);
}
void dfs2(int x,int fa,bool del)
//求解,del表示求完x的子树的答案后需不需要清空x的子树的信息
{
	for(int i=first[x];i;i=e[i].next)//先统计轻儿子的答案
	if(e[i].y!=fa&&e[i].y!=mson[x])dfs2(e[i].y,x,true);
	if(mson[x]!=0)dfs2(mson[x],x,false);//最后统计重儿子的答案
	
	tong[col[x]]++;if(tong[col[x]]==1)now_ans++;//统计自己以及轻子树的信息
	for(int i=first[x];i;i=e[i].next)
	if(e[i].y!=fa&&e[i].y!=mson[x])go(e[i].y,x,1);
	ans[x]=now_ans;//得到自己的答案
	
	if(del)go(x,fa,-1);//假如要删掉自己的信息,就暴力地删掉
}

int main()
{
	scanf("%d",&n);
	for(int i=1,x,y;i<n;i++)
	scanf("%d %d",&x,&y),buildroad(x,y),buildroad(y,x);
	for(int i=1;i<=n;i++)
	scanf("%d",&col[i]);
	dfs1(1,0);
	dfs2(1,0,false);
	scanf("%d",&m);
	for(int i=1,x;i<=m;i++)
	scanf("%d",&x),printf("%d\n",ans[x]);
}

例题2

题目传送门

还是一颗节点有颜色的树,需要统计每个棵子树内出现次数最多的颜色之和。

板子题,直接上代码:

#include <cstdio>
#include <cstring>
#define maxn 100010

int n,col[maxn];
struct edge{int y,next;};
edge e[maxn*2];
int first[maxn];
void buildroad(int x,int y)
{
	static int len=0;
	e[++len]=(edge){y,first[x]};
	first[x]=len;
}
int size[maxn],mson[maxn];
void dfs_getmson(int x,int fa)
{
	size[x]=1;
	for(int i=first[x];i;i=e[i].next)
	{
		int y=e[i].y;
		if(y==fa)continue;
		dfs_getmson(y,x);
		if(size[y]>size[mson[x]])mson[x]=y;
		size[x]+=size[y];
	}
}
long long ans[maxn],now_ans=0;
int tong[maxn],max_col=0;
void go(int x,int fa,int type)
{
	tong[col[x]]+=type;
	if(type==1)
	{
		if(tong[col[x]]>max_col)max_col=tong[col[x]],now_ans=col[x];
		else if(tong[col[x]]==max_col)now_ans+=col[x];
	}
	for(int i=first[x];i;i=e[i].next)
	if(e[i].y!=fa)go(e[i].y,x,type);
}
void dfs_getans(int x,int fa,bool del)
{
	for(int i=first[x];i;i=e[i].next)
	if(e[i].y!=fa&&e[i].y!=mson[x])dfs_getans(e[i].y,x,true);
	
	if(mson[x]!=0)dfs_getans(mson[x],x,false);
	for(int i=first[x];i;i=e[i].next)
	if(e[i].y!=fa&&e[i].y!=mson[x])go(e[i].y,x,1);
	
	tong[col[x]]++;
	if(tong[col[x]]>max_col)max_col=tong[col[x]],now_ans=col[x];
	else if(tong[col[x]]==max_col)now_ans+=col[x];
	
	ans[x]=now_ans;if(del)go(x,fa,-1),max_col=0,now_ans=0;
}

int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	scanf("%d",&col[i]);
	for(int i=1,x,y;i<n;i++)
	scanf("%d %d",&x,&y),buildroad(x,y),buildroad(y,x);
	dfs_getmson(1,0);
	dfs_getans(1,0,false);
	for(int i=1;i<=n;i++)
	printf("%lld ",ans[i]);
}
  • 14
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值