DSU ON TREE

什么是DSU on tree ? 就是树上启发式合并,启发式合并就是两个集合,按照常规思路 我们是将小的集合并到大的集合里面去,依次合并所有的集合。可以证明这是logn 的时间复杂度。所谓树上启发式合并就是,在树上进行操作。

这里讲三个题分别是

CF 600 E Lomsat gelral

P4149 [IOI2011] Race

P9233 [蓝桥杯 2023 省 A] 颜色平衡树

首先第一题非常经典。

大意就是 有一颗树,统计 所有子树占主导的所有编号之和,所谓占主导就是,就是出现次数最多的那个编号。如果次数一样多,那么就都是众数。

我们如何思考,一般树上问题都是递归解决。

递归 最重要的是  终止条件 ,和本层需要做的工作,这两点做好,递归每一层都是一样的,

这里可以看看灵神的视频 看到递归就晕?带你理解递归的本质!

在了解递归之后,我们应该怎么做呢,暴力的思路就是,统计每个以某个节点为根的子树,暴力搜索所有节点,统计,然后在处理答案,这样时间复杂度是O(n²)的

优化思路:

在dsu on tree 中 我们每一个以某节点为根的树中,有若干儿子,一些儿子的包含的节点个数多 我们称之为重儿子,节点个数少的称为轻儿子。 根据启发式合并的思想,我们把轻儿子合并到重儿子中,在统计,所以 我们可以保留重儿子的信息,消去轻儿子的信息。 这样可以证明时间复杂度是O(nlogn) 的,可以去看看大佬证明。

总而言之,dsu on tree  格式大差不差,最重要的是 我们对不同的题目需要保存不同的信息。这点是不同的。最重要思考的是保存什么样的信息。根据题意来。

dsu 板子如下 

首先是 dfs 函数 // 统计所需要的信息 ,如 dfs 序 重儿子 是哪个,等等

void dfs_init (int u,int f){
	l[u] = ++tot;
	id[tot] = u;
	// id是存第几个序列是什么节点 l是存这个节点起始是第几序列 
	sz[u] = 1 ;
	hs[u] = -1;
	for (auto v : e[u]){
		if(v==f)continue;
		dfs_init(v,u);
		sz[u] +=sz[v];
		if(hs[u] == -1 || sz[v] > sz[hs[u]])
		     hs[u]=v;
	}
	r[u] = tot;
} 

之后在进行一次 dfs  便是dsu on tree 

void dfs_solve(int u ,int f,bool keep){
	for(auto v :e[u]){
		if ( v!= f && v!=hs[u]){
			dfs_solve(v,u,false); 
		}
	}
	if(hs[u] != -1){
		dfs_solve(hs[u],u,true);
		// 重儿子的集合 
	}
	auto add =[&](int x){
		x = c[x];
		cnt[x]++;
		if(cnt[x] > maxcnt) maxcnt = cnt[x],sumcnt=0;
		if (cnt[x] == maxcnt) sumcnt+=x;
	};
	auto del = [&](int x){
		x = c[x];
		cnt[x]--;
	};
	for (auto v : e[u]){
		if(v!=f && v!= hs[u]){
			for(int x =l[v];x<=r[v];x++)
			 add(id[x]);
		}
	} 
	// u本身加入
	add(u);
	ans[u] = sumcnt;
	if(!keep){
		maxcnt = 0;
		sumcnt = 0;
		for(int x = l[u];x<=r[u];x++){
			del(id[x]);
		} 
	} 
}

注意 这第二次dfs  根据不同的题目进行修改 。

下面贴出完整代码。

# include<bits/stdc++.h>

using namespace std;

typedef long long LL;
const int N = 101000;
int n;、 
vector<int>e[N];
int l[N],r[N],id[N],sz[N],hs[N],tot,c[N];
int cnt[N];//每个颜色出现次数 
int maxcnt;//众数出现次数 
LL sumcnt,ans[N]; //众数的和 
// dfs 序列 
void dfs_init (int u,int f){
	l[u] = ++tot;
	id[tot] = u;
	// id是存第几个序列是什么节点 l是存这个节点起始是第几序列 
	sz[u] = 1 ;
	hs[u] = -1;
	for (auto v : e[u]){
		if(v==f)continue;
		dfs_init(v,u);
		sz[u] +=sz[v];
		if(hs[u] == -1 || sz[v] > sz[hs[u]])
		     hs[u]=v;
	}
	r[u] = tot;
} 

void dfs_solve(int u ,int f,bool keep){
	for(auto v :e[u]){
		if ( v!= f && v!=hs[u]){
			dfs_solve(v,u,false); 
		}
	}
	if(hs[u] != -1){
		dfs_solve(hs[u],u,true);
		// 重儿子的集合 
	}
	auto add =[&](int x){
		x = c[x];
		cnt[x]++;
		if(cnt[x] > maxcnt) maxcnt = cnt[x],sumcnt=0;
		if (cnt[x] == maxcnt) sumcnt+=x;
	};
	auto del = [&](int x){
		x = c[x];
		cnt[x]--;
	};
	for (auto v : e[u]){
		if(v!=f && v!= hs[u]){
			for(int x =l[v];x<=r[v];x++)
			 add(id[x]);
		}
	} 
	// u本身加入
	add(u);
	ans[u] = sumcnt;
	if(!keep){
		maxcnt = 0;
		sumcnt = 0;
		for(int x = l[u];x<=r[u];x++){
			del(id[x]);
		} 
	} 
}
// dfs序是真的牛逼 
int main(){
	scanf("%d",&n);
	for(int i = 1 ;i<=n;i++){
		scanf("%d",&c[i]);
	}
	// 建树吧 
	for(int i = 1;i<n;i++){
		int u,v;
		scanf("%d%d",&u,&v);
		e[u].push_back(v);
		e[v].push_back(u);
	}
	dfs_init(1,0);
	dfs_solve(1,0,false);
	for(int i = 1;i<=n;i++){
		printf("%lld%c",ans[i]," \n"[i==n]);
	}
}



那么第二题,我们应该如何思考呢?

题目是 给一棵树,每条边有权。求一条简单路径,权值和等于 k,且边的数量最小。

还是一样的,对于当前节点为根的树, 我们所需要的是 在保存重儿子之后, 依次暴力遍历轻儿子,因为 两点间的权值和  是  dfs(u) + dfs(v) - 2dfs(lca(u,v)) == k  dfs(u) 这里定义为 根到u的权值   lca 是u和v 的最近公共祖先。 所以在遍历轻儿子时,我们查询是否存在这个权值 ,使得 d = k + 2dfs(lca(u,v)) - dfs(v) 存在 则比较边数是否跟已有的还要小,如果小,则更新全局变量,最后在所有节点遍历完后 输出答案即可。 下面是代码

# include<bits/stdc++.h>

using namespace std;

typedef long long LL;
const int N = 200010;
int n,k;
vector<pair<int,int>> e[N];
int l[N],r[N],id[N],sz[N],hs[N],tot,c[N];

int dep1[N];
LL dep2[N];

int ans;
map<LL,int> val;
// 板子 

void dfs_init(int u,int f){
	l[u] = ++tot;
	id[tot] = u;
	sz[u] = 1;
	hs[u] = -1;
	for(auto[v,w] : e[u]){
		if(v==f) continue;
		dep1[v] = dep1[u] + 1;
		dep2[v] = dep2[u] + w ;// dep2存的应该是权值
		dfs_init(v,u);
		sz[u] += sz[v];
		if(hs[u] == -1 || sz[v] > sz[hs[u]])
		  hs[u] = v;
	}
	r[u] = tot;
}
void dfs_solve(int u,int f,bool keep){
	  for(auto [v,w] : e[u]){
	  	  //先处理轻儿子
			if(v!=f && v!=hs[u]){
				dfs_solve(v,u,false);
			} 
	  }
	  //处理重儿子
	  if(hs[u] != -1){
	  	dfs_solve(hs[u],u,true);
	  } 
	 // 编写查询函数
	 auto query = [&](int w){
	 	LL d2 =  k + 2*dep2[u] - dep2[w];
	 	if (val.count(d2)){
	 		ans = min(ans,val[d2]+dep1[w]-2*dep1[u]);
		 }
	 };
	auto add = [&](int w){
		if (val.count(dep2[w]))
		   val[dep2[w]] = min(val[dep2[w]],dep1[w]);
		else 
		    val[dep2[w]] = dep1[w];
	};
	
	//开始遍历轻儿子了
	for(auto [v,w] : e[u]){
		if (v != f && v!= hs[u]){
			for(int x =l[v];x<=r[v];x++){
				query(id[x]);
			}
			for(int x = l[v];x<=r[v];x++)
			   add(id[x]);
		}
	}
	query(u);
	add(u);
	if(!keep){
		val.clear();
	} 
} 
// 递归的本质 就是分别处理每棵子树 保留重儿子,清除轻儿子, 最后一遍遍历轻儿子是处理 以u为根节点的子树哦 

int main(){
	scanf("%d%d",&n,&k);
	for(int i = 1 ;i<n;i++){
		int u ,v,w;
		scanf("%d%d%d",&u,&v,&w);
		++u;
		++v;
		// 他是0开始编号直接自己加1就行。 
		e[u].push_back({v,w});
		e[v].push_back({u,w});
	}
	ans = n+ 1;
	dfs_init(1,0);
	dfs_solve(1,0,false);
	if(ans >=n+1){
		ans = -1;
	}
	printf("%d",ans);
	
	
	return 0;
} 

第三题 lqb 的题目  跟第一题几乎是一样的 ,板子题,我们只需要统计众数*众数的数目 是否等于以该点为根的节点个数 如果是 ans + 1 ,代码如下。

# include<bits/stdc++.h>

using namespace std;

typedef long long LL;
const int N = 200010;
int n;
vector<int>e[N];
int cnt[N];//每个颜色出现次数 
int maxcnt;//众数出现次数 
LL sumcnt,ans; //众数的和  众数出现多少次 
int l[N],r[N],id[N],sz[N],hs[N],tot,c[N];


void dfs(int u,int f){
		l[u] = ++tot;
		id[tot] = u;
		sz[u] = 1;
		hs[u] = -1;
		for ( auto v : e[u]){
			if (v == f) continue;
			dfs(v,u);
			// 一路递归上去
			sz[u] += sz[v];
			if(hs[u] == -1 || sz[v] > sz[hs[u]]){
				hs[u] = v;
			} 
		}
		// 结束的dfs序列 
		r[u] = tot; 
} 
void dfs2(int u,int f ,bool keep){
	for(auto v :e[u]){
		if ( v!= f && v!=hs[u]){
			dfs2(v,u,false); 
		}
	}
	if(hs[u] != -1){
		dfs2(hs[u],u,true);
		// 重儿子的集合 
	}
		auto add =[&](int x){
		x = c[x];
		cnt[x]++;
		if(cnt[x] > maxcnt) maxcnt = cnt[x],sumcnt=0;
		// 这里有问题 应该是else if 原本是统计颜色的编号所以 改完之后 要立即加上 ,但是这个不用 判断 如果相等在家家,
		//这样导值不对 不然我可以令他等于0 
		if (cnt[x] == maxcnt) sumcnt++;
	};
	auto del = [&](int x){
		x = c[x];
		cnt[x]--;
	};
		for (auto v : e[u]){
		if(v!=f && v!= hs[u]){
			for(int x =l[v];x<=r[v];x++)
			 add(id[x]);
		}
	} 
	// u本身加入
	add(u);
	if (maxcnt*sumcnt == sz[u]){
		ans++;
	}
	if(!keep){
		maxcnt = 0;
		sumcnt = 0;
		for(int x = l[u];x<=r[u];x++){
			del(id[x]);
		} 
	} 
}

int main(){
	scanf("%d",&n);
	int a,b;
	for(int i = 1;i<=n;i++){
		scanf("%d%d",&a,&b);
		c[i] = a;
		if (b!=0){
	   	e[b].push_back(i);
	 	e[i].push_back(b);
		}
	}
	dfs(1,0);
	dfs2(1,0,false);
	cout<<ans<<endl; 
	return 0;
}

总结:dsu on tree 最重要的是维护什么样子的信息,如何将题目要求翻译过来, dsu on tree 的题目套路都差不多。

  • 18
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值