树上启发式合并【含2020CCPC长春F题等稍微变种】

本文介绍了树上启发式合并(DsuOnTree)算法,一种用于高效解决树上子树计数问题的方法,通过剪枝技巧将时间复杂度优化至nlogn。通过实例演示如何在查询不同颜色子树数量和点对权值异或问题中应用此算法,展示了其在暴力查询优化中的关键作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

学习了树上启发式合并一段时间了,我来写一个总结吧;
树上启发式合并叫做Dsu On Tree
被某个毒瘤大佬称为静态链分治;

树上启发式合并我也不知道为什么叫启发式。
但是在我的理解中树上启发式合并和莫队分块都是一样的暴力的机制。是一种合理分配查询的顺序来节省资源的做法;

莫队分块可以在多次询问中合理安排询问的顺序,导致时间变成n*sqrt(n);
而树上启发式合并则更加强悍,时间复杂度为nlogn;

首先来一手板题:
给你一个树,询问每一个结点子树中含有不同颜色的数有多少种;颜色也是与n一个数量级,设都是1e5吧;
首先想到的是暴力,但是如此会爆空间,每一个点都开一个1e5的桶一定是会炸的;
如果使用一个桶,那么每次求完我们都要清空桶,然后在新的子树中的信息放到这个已经清空的桶中去;

如此便是一个N*N的算法,非常的暴力;

但是我们可以加一个显而易见的剪枝:容易得知:当K子树儿子都给求出来之后,此时他的一个儿子的信息还留在桶里哪,我们不用清空这个桶,直接保留这些信息,然后把K子树其他点放进去来更新答案;

然后就出现了一个非常简单的剪枝:那就是对于K子树的答案,先求出K的轻儿子的答案,然后遍历轻儿子来取出轻儿子在桶中的影响,然后求重儿子的答案,此时不去清空影响,而是保留桶中重儿子的信息来作为K子树的信息的基底。

于是,时间复杂度就神奇的变成了O(NlogN)!!!!!!!!

是不是很神奇;仅仅是不清空重儿子保留其在桶中的信息而已,为什么就能产生如此巨大的剪枝影响。

由于重儿子的信息是不会被清除的,那么非常显然的一点就是当自底向上的过程中,每一个轻儿子都只会进最多Logn次桶,出Logn次桶;如此便能够保证时间复杂度;

E. Lomsat gelral
对于这个题显然就是和上面的原始问题是一样的题目;题意为:每个节点出现的最多点的编号和。

同时DsuOnTree有很多写法,我这里推荐的是更格式化且易懂的做法;

#include<bits/stdc++.h>
const double eps = 1e-6;
const double PI = acos(-1);
const int INF = 0x3f3f3f3f;
#define ms(a,k) memset(a,k,sizeof(a))
#define X first
#define Y second
#define pii pair<int ,int >
#define lowbit(a) a&(-a)
typedef long long ll ;
typedef unsigned long long ull;
using namespace std;
const int mod = 1e9+7;
const int maxn = 1e5+10;
inline void read(int &x){scanf("%d",&x);}
int c[maxn],sz[maxn],tong[maxn];
ll sum,_max,ans[maxn];
vector<int >g[maxn];
int dfs(int k,int last)
{//求子树size,用来求出来他们的重儿子和轻儿子
	sz[k] = 1;
	for(int i =0;i<g[k].size();++i)
	{
		if(g[k][i]!=last)sz[k]+=dfs(g[k][i],k);
	}
	return sz[k];
}
bool cmp(int x,int y){return sz[x]>sz[y];}
void _merge(int k)
{//把k子树的信息合并到桶中并更新答案
	tong[c[k]]++;
	if(tong[c[k] ]>_max){sum = c[k];_max = tong[c[k] ];}
	else if(tong[c[k] ]==_max){sum +=c[k];}
	for(int i =1;i<g[k].size();++i)
	{
		_merge(g[k][i]);
	}
}
void change(int k)
{//入桶函数与出桶函数共用一个函数体,增加一个参数cut=-1或者1就能控制tong[c[k] ]的变化;
	tong[c[k] ]--;
	for(int i =1;i<g[k].size();++i)change(g[k][i]);
}
void dsu(int k)
{//DsuOnTree的主体函数;
	//printf("%d\n",k);
	int len = g[k].size();
	for(int i =len-1;i>=1-(k==1);--i)
	{//首先处理的是儿子们的答案,当g[k][i]为重儿子的时候就不清空桶,因为我在main里面排序了,所以重儿子就是1-(k==1),这里要减去一个(k==1)自然是因为1是没有father结点的;
		sum =0;_max = 0;
		dsu(g[k][i]);
		if(i != (1-(k==1)) )change(g[k][i]);//重儿子的信息保留,其他都删除;
	}
	for(int i =len-1;i>1-(k==1);--i)
	{
		_merge(g[k][i]);
	}
	tong[c[k]]++;
	if(tong[c[k]]>_max){sum = c[k];_max = tong[c[k] ];}
	else if(tong[c[k]]==_max){sum+=c[k];}
	ans[k] = sum;
}
int main()
{
	int n;read(n);
	for(int i =1;i<=n;++i)read(c[i]);
	for(int i =1;i<n;++i)
	{
		int u,v;read(u);read(v);
		g[u].push_back(v);
		g[v].push_back(u);
	}
	dfs(1,1);
	for(int i=1;i<=n;++i)sort(g[i].begin(),g[i].end(),cmp);
	//从大到小排序,如此0就是father,1就是重儿子,其他就是轻儿子;
	//这里如果觉得时间复杂度多起来了的话可以dfs一遍找出father和重儿子然后和0或1交换即可,但是因为本题没有怎么卡这个时间,所以我就直接排序了,写一个对重儿子和父亲节点的初始化函数会比较长。
	dsu(1);
	for(int i =1;i<=n;++i)printf(" %lld"+(i==1),ans[i]);
	printf("\n");

	return 0;
}

这个是我觉得比较合理的Dsu写法,因为这个函数化的写法比main函数一大堆东西或者dsu函数一堆东西的写法更加清晰有层次;

这个题纯粹就是熟悉最基础的Dsu题目。

下面还有变种:很多树上计数问题都能转化为DSU;

F - Strange Memory
长春的F题问的是点对ij满足权值异或等于他们lca的权值,贡献为i^j;

这个题如果按照莽夫dsu的解法是不行的,因为对于aj ,他要找到权值 a j X O R a l c a a_j XORa_{lca} ajXORalca所在的各种i,而这各种i就是棘手的地方,因为i都是不相同的,你求的却是I XOR J的值,于是你就必须把i给分解掉;用tong[V][L][F]表示权值为V的众多i,在第L为为F的有多少个;如此就能够按位算权值,于是时间变成了 N ( L o g N ) 2 N(LogN)^2 N(LogN)2;
Code:

#include<bits/stdc++.h>
const double eps = 1e-6;
const double PI = acos(-1);
const int INF = 0x3f3f3f3f;
#define ms(a,k) memset(a,k,sizeof(a))
#define X first
#define Y second
#define pii pair<int ,int >
#define lowbit(a) a&(-a)
typedef long long ll ;
typedef unsigned long long ull;
using namespace std;
inline void read(int &x){scanf("%d",&x);}
const int mod = 1e9+7;
const int maxn = 1e5+10;
int a[maxn],tong[maxn*10][21][2],sz[maxn];//tong[v][l][f] 表示权值为v的数的下标在第l位上0的数量和1的数量;
ll ans =0;
vector<int >g[maxn];
int dfs(int k,int last)
{
	sz[k] = 1;
	for(int i =0;i<g[k].size();++i)
	{
		if(g[k][i]!=last)sz[k] += dfs(g[k][i],k);
	}
	return sz[k];
}
bool cmp(int x,int y){return sz[x]>sz[y];}
void _merge(int k,int lca)
{//合并k子树的信息
	if ( (a[lca]^a[k])<=1000000)
	{
		int rem = k;
		ll base = 1;
		for(int i =0;i<=20;++i)
		{
			//printf("*%d %d %d\n",i,rem&1^1,tong[a[lca]^a[k] ][i][rem&1^1] );
			ans += (ll )tong [ a[lca]^a[k] ][i][(rem&1)^1]*base;
			//rem^1表示某位上与rem&1不同才有贡献,贡献就是tong的数值,乘上base即为贡献;
			rem/=2;
			base*=2;
		}
	}

	for(int i =1;i<g[k].size();++i)_merge(g[k][i],lca);
}
void in(int k,int cut)
{
	int rem = k;
	for(int i =0;i<=20;++i)
	{
		tong[a[k] ][i][rem&1] += cut;//cut为-1则为出桶,为1则是入桶
		rem/=2;//更新rem
	}
}
void change(int k,int cut)
{
	in(k,cut);
	for(int i =1;i<g[k].size();++i)change(g[k][i],cut);
}
void dsu(int k)
{
	int len = g[k].size();
	for(int i = len-1;i>=1-(k==1);--i)
	{
		//printf(" %d %d\n",k,g[k][i]);
		dsu(g[k][i]);
		if( i!= (1-(k==1)) )change(g[k][i],-1);
	}
	for(int i = len-1;i>1-(k==1);--i)
	{
		_merge(g[k][i],k);
		change(g[k][i],1);
	}
	int rem = k;
	ll base = 1;
	for(int i =0;i<=20;++i)
	{
		ans += (ll )tong [ 0 ][i][ (rem&1)^1 ]*base;//如果某个地方是0,那么和任何数异或都是本身;
		rem>>=1;
		base<<=1;
	}
	in(k,1);//放入k;
}
int main()
{
	int n;read(n);
	for(int i =1;i<=n;++i)read(a[i]);
	for(int i =1;i<n;++i)
	{
		int u,v;read(u);read(v);
		g[u].push_back(v);
		g[v].push_back(u);
	}
	dfs(1,1);
	for(int i =1;i<=n;++i)sort(g[i].begin(),g[i].end(),cmp);
	dsu(1);
	printf("%lld\n",ans);
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值