【图论】树上启发式合并

本篇博客参考:

文章目录

基本概念

首先,什么是 启发式合并

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

树上启发式合并(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]);
}
相关推荐
转基因1 小时前
AtCoder Beginner Contest 403(题解ABCDEF)
数据结构·c++·算法
冠位观测者1 小时前
【Leetcode 每日一题】2071. 你可以安排的最多任务数目
数据结构·算法·leetcode
到底怎么取名字不会重复1 小时前
Day15(贪心算法)——LeetCode121.买卖股票的最佳时机&55.跳跃游戏
c++·算法·leetcode·游戏·贪心算法
wuqingshun3141593 小时前
蓝桥杯 11. 最大距离
数据结构·c++·算法·职场和发展·蓝桥杯
Dovis(誓平步青云)3 小时前
【数据结构】励志大厂版·初阶(复习+刷题):栈与队列
c语言·开发语言·数据结构·经验分享·笔记·学习·算法
大魔王(已黑化)4 小时前
LeetCode —— 94. 二叉树的中序遍历
数据结构·c++·算法·leetcode·职场和发展
六点半8885 小时前
【蓝桥杯】第十六届蓝桥杯C/C++大学B组个人反思总结
c语言·c++·算法·蓝桥杯
✿ ༺ ོIT技术༻5 小时前
笔试强训:Day3
c++·笔记·算法
n33(NK)6 小时前
【算法基础】冒泡排序算法 - JAVA
java·算法·排序算法
珊瑚里的鱼8 小时前
牛客网题解 | 栈的压入、弹出序列
开发语言·c++·笔记·算法·leetcode·stl