【图论】树上启发式合并

本篇博客参考:

文章目录

基本概念

首先,什么是 启发式合并

有人将其称为"优雅的暴力",启发式合并就是在合并两个部分的时候,将内容少的部分合并至内容多的部分,减少合并的操作时间

树上启发式合并(dsu on tree) 可以被用来解决树上的 离线问题 (请注意,必须要是离线问题,因为处理问题的顺序有讲究),特别是可以维护以每个点为根的子树中的信息

一般来说,对于查询以每个点为根的子树中的信息的问题,我们可以用树形dp来处理,但是如果每个点的信息不止一两个数字,而是很庞大的部分(比如说每个点所需要的信息都要多个map来存储),这样使用树形dp的空间复杂度将会非常庞大,而树上启发式合并可以用来解决这样的问题

代码实现

举个例子,比如说我们给出一棵树,树上的每个结点染色,现在我们需要统计以每个结点为根的子树中出现多少种颜色

最暴力的方法就是每个结点跑一次 dfs,用 cnt[] 数组存储每个颜色出现的次数,输出

但是很明显会T的很惨

这样的树中,我们首先计算2子树的信息,然后计算3子树的信息的时候我们又要把2子树清空,每计算一个新的子树都要把之前计算过的信息清空,根本存不下来信息啊

然后我们考虑一下怎么优化呢,父结点的信息和子结点相关,我们可以用子结点的信息更新父结点的信息,也就是,我们在计算1子树的所有结点信息时,假如4子树是234里最后一个被计算的,那我们算完4子树之后,可以不用清空 cnt 数组,反正我们计算1子树的时候还是要遍历4子树的,将4子树的信息全部保留,再加上前面23子树的信息就可以得到1子树的信息了

这个4子树应该怎么选择呢?换句话说,我们保留哪一个子树的信息不被删除呢?根据启发式合并的思想,保留最庞大的子树信息不动,就可以减少重复计算的次数了

在树链剖分时我们把树中结点最多的子树根结点叫做重子结点,也就是说,在树上启发式合并的过程中,我们需要先计算所有轻子结点的信息(每计算一个轻子结点之后都要删除这个结点对当前答案的影响),最后计算重子结点的信息(保留重子结点对当前答案的影响),然后再计算前面的轻子结点(这一次计算要保留结点对当前答案的影响)

用两遍 dfs 实现

下面是一些变量定义:

  • sz[u] 以 u 为根的子树的结点数量
  • son[u] 结点 u 的重子结点
  • col[u] 结点 u 的颜色
  • L[u] 结点 u 的 dfs 序
  • R[u] 以 u 为根的子树中结点 dfs 序的最大值
  • id[u] L 标号 u 对应的结点编号,有 id[L[u]] == u
  • cnt[u] 颜色 u 的出现次数
  • totcol 目前出现过的颜色个数
cpp 复制代码
void dfs1(int u, int fa) // u: 当前结点  fa: 父结点
{
	L[u] = ++ totdfn; // 更新u的dfs序
	Node[totdfn] = u; // 更新dfs序的映射
	sz[u] = 1; // 初始化子树大小为1
	for (int i = 0; i < g[u].size(); i ++ )
	{
		int j = g[u][i]; // 子结点编号
		if (j == fa) continue;
		dfs1(j, u);
		sz[u] += sz[j]; // 用子结点的sz更新父结点的sz
		if (sz[j] > sz[son[u]]) son[u] = j; // 更新重子结点
	}
	R[u] = totdfn; // 更新当前子树中dfs序的最大值
}

void dfs2(int u, int fa, bool keep) // u: 当前结点  fa: 父结点  keep: 此次遍历计算的答案是否保留
{
	// 计算轻子结点的答案
	for (int i = 0; i < g[u].size(); i ++ )
	{
		int j = g[u][i]; // 子结点编号
		if (j == fa || j == son[u]) continue; // 遇到重子结点或者父结点就跳过
		dfs2(j, u, false); // 继续计算轻子结点的答案且不保留
	}

	if (son[u]) dfs2(son[u], u, true); // 计算重儿子答案并保留计算过程中的数据(用于继承)

	for (int i = 0; i < g[u].size(); i ++ )
	{
		int j = g[u][i]; // 子结点编号
		if (j == fa || j == son[u]) continue; // 遇到重子结点或者父结点就跳过

		// 子树结点的 DFS 序构成一段连续区间,可以直接遍历
		for (int k = L[j]; k <= R[j]; k ++ ) add(id[k]); // 加上轻子结点对答案的贡献
	}
	add(u); // 加上当前子树根结点对答案的贡献
	ans[u] = totcol;
	if (keep == false) // 如果当前计算的答案不保留 就删去
		for (int i = L[u]; i <= R[u]; i ++ ) del(id[i]);
}
相关推荐
xiaoshiguang33 小时前
LeetCode:222.完全二叉树节点的数量
算法·leetcode
爱吃西瓜的小菜鸡3 小时前
【C语言】判断回文
c语言·学习·算法
别NULL3 小时前
机试题——疯长的草
数据结构·c++·算法
TT哇3 小时前
*【每日一题 提高题】[蓝桥杯 2022 国 A] 选素数
java·算法·蓝桥杯
yuanbenshidiaos5 小时前
C++----------函数的调用机制
java·c++·算法
唐叔在学习5 小时前
【唐叔学算法】第21天:超越比较-计数排序、桶排序与基数排序的Java实践及性能剖析
数据结构·算法·排序算法
ALISHENGYA5 小时前
全国青少年信息学奥林匹克竞赛(信奥赛)备考实战之分支结构(switch语句)
数据结构·算法
chengooooooo5 小时前
代码随想录训练营第二十七天| 贪心理论基础 455.分发饼干 376. 摆动序列 53. 最大子序和
算法·leetcode·职场和发展
jackiendsc5 小时前
Java的垃圾回收机制介绍、工作原理、算法及分析调优
java·开发语言·算法
游是水里的游6 小时前
【算法day20】回溯:子集与全排列问题
算法