CF600E Lomsat gelral(树上启发式合并)

本文介绍了一种解决树形结构问题的高效算法——树上启发式合并(dsudsu on tree),通过保留重儿子结果并暴力迭代轻儿子,将复杂度优化至O(nlogn)。文章详细讲解了算法原理,包括重儿子概念、复杂度分析及代码实现。

题意

有一颗 nnn 个节点的树,以 111 为根节点,每个点有一个颜色 viv_ivi。设子树 aaa 中颜色出现次数最多的颜色集合为 {bi}\{b_i\}{bi},记 ansa=∑bians_a=\sum b_iansa=bi。现在要求 ans1,ans2,....ansnans_1,ans_2,....ans_nans1,ans2,....ansn
其中,n,v≤105n,v\leq 10^5n,v105

分析

这种题叫做 dsu on treedsu~on~treedsu on tree,也就是树上启发式合并。
让我们先考虑暴力做法。
就是以每个节点,对子树进行 dfsdfsdfs,然后开一个桶记录颜色出现次数,最后把颜色出现次数最多的颜色加起来。这样子做复杂度是 O(n2)O(n^2)O(n2) 的。
这复杂度显然是不可接受的嘛!暴力差就差在,它计算了很多重复的东西!如果我们能让重复的东西尽量减少计算,复杂度就能够得到提升了!



看到这道题,有的同学可能一下子想的是树上莫队。
确实,莫队算法就是用来优化这些有重复计算的东西的。
不过,更优秀的算法是用启发式合并,复杂度可以做到 O(nlogn)O(nlogn)O(nlogn)
一句话解释这个算法,就是保留重儿子的结果,暴力迭代轻儿子。



重儿子是什么?
如果你学过树链剖分,就能一下子知道了。不过没学过也没关系。重儿子就是这个节点所有儿子中 sizsizsiz 最大的点。如图:
在这里插入图片描述
xxxsizsizsiz 最大,所以 xxxrtrtrt 的重儿子。
totitot_itoti 为颜色 iii 的出现次数。
我们要让重复求的东西尽量少,但是子树之间又互相独立,于是我们只能钦点一个子树来保留 tottottot 的值。既然重儿子如此牛逼,那我们就钦点重儿子吧!
假设我们现在要求 ansrtans_{rt}ansrt,我们已经保留了 xxx 子树的 tottottot 数组。
那我们从 rtrtrt 开始遍历一遍子树,如果遇到 y,zy,zy,z,就继续往下 dfsdfsdfs,求出 tottottot。如果遇到 xxx,那么就可以 returnreturnreturn 了,因为之前求过 tottottot 了。再求一遍不是智障了吗??
这样子,单次求 ansrtans_rtansrt 的复杂度是 sizrt−sizxsiz_{rt}-siz_{x}sizrtsizx 的。
写成代码的话长这样:
在这里插入图片描述



关于复杂度

不妨考虑每个点会被访问多少次。
如果一个节点到根节点有 xxx 条轻边,那么这个节点会被访问 xxx 次。
由于一个节点到根节点的轻边数量不超过 lognlognlogn 条。
于是总的复杂度为 O(nlogn)O(nlogn)O(nlogn)



后记

为什么这个叫树上启发式合并呢?其实,求解的过程可以看作是轻儿子不断往重儿子合并的过程,这和常规的启发式合并是一致的。从这个角度来看,复杂度也是 O(nlogn)O(nlogn)O(nlogn)

代码如下

#include <bits/stdc++.h>
#define N 100005
using namespace std;
typedef long long LL;
typedef unsigned long long uLL;
LL z = 1;
int read(){
	int x, f = 1;
	char ch;
	while(ch = getchar(), ch < '0' || ch > '9') if(ch == '-') f = -1;
	x = ch - '0';
	while(ch = getchar(), ch >= '0' && ch <= '9') x = x * 10 + ch - 48;
	return x * f;
}
struct node{
	int a, b, n;
}d[N * 2];
int fa[N], siz[N], son[N], h[N], v[N], cnt;
int tot[N], Son, maxn;
LL ans[N], sum;
void cr(int a, int b){
	d[++cnt].a = a; d[cnt].b = b; d[cnt].n = h[a]; h[a] = cnt;
}
void dfs1(int a){
	int i, b;
	siz[a] = 1;
	for(i = h[a]; i; i = d[i].n){
		b = d[i].b;
		if(b == fa[a]) continue;
		fa[b] = a;
		dfs1(b);
		siz[a] += siz[b];
		if(siz[b] >= siz[son[a]]) son[a] = b;//找到重儿子 
	}
}
void add(int a, int c){//遍历 a 的子树,求出 ans[a] 
	int i, b;
	tot[v[a]] += c;//更新 tot 数组 
	if(maxn < tot[v[a]]) maxn = tot[v[a]], sum = v[a];
	else if(maxn == tot[v[a]]) sum += v[a];//这一步是在更新 sum 和 maxn
	for(i = h[a]; i; i = d[i].n){
		b = d[i].b;
		if(b == fa[a] || b == Son) continue;//遇到重儿子就return,所以只遍历轻儿子 
		add(b, c);
	}
}
void dsu(int a, int flag){
	int i, b;
	for(i = h[a]; i; i = d[i].n){
		b = d[i].b;
		if(b != fa[a] && b != son[a]) dsu(b, 1);//先求轻儿子 
	}
	if(son[a]) dsu(son[a], 0), Son = son[a]; //再求重儿子 
	add(a, 1); Son = 0; ans[a] = sum;//求出 ans[a],同时把重儿子标记去除(没去除的话无法清空 tot 数组 
	if(flag) add(a, -1), sum = 0, maxn = 0;//如果当前节点是轻儿子,就清空 tot 数组并且重置 sum 和 maxn 
}
int main(){
	int i, j, n, m, a, b;
	n = read();
	for(i = 1; i <= n; i++) v[i] = read();
	for(i = 1; i < n; i++){
		a = read(); b = read();
		cr(a, b); cr(b, a);
	}
	dfs1(1);
	dsu(1, 0);
	for(i = 1; i <= n; i++) printf("%lld ", ans[i]);
	return 0;
}
<think>嗯...用户这次问的是"启发式合并"和"树上启发式合并"的优劣区别,看来ta对算法优化领域很感兴趣啊。从ta精准引用站内文献的能力看,可能是计算机专业的研究生或从业者。 回顾上次对话,用户系统要求很严格呢,必须确保所有数学公式都正确用$包裹,中文回答,还要生成相关问题。不过这次问题本身倒很聚焦,就是两个概念的对比分析。 让我先理清核心概念:普通启发式合并更通用,适用于集合合并问题;而树上启发式合并(dsu on tree)是专门处理树形结构的变种。有意思的是,引用[1]提到dsu on tree用于"统计子树中具有某种特征的节点数",这正好解释了它的专精领域。 注意到引用[2][3][4]其实更侧重"启发式搜索",与当前问题关联度不高。重点应该放在引用[1]和引用[3]的对比上——前者描述树上版本,后者定义通用版本。用户特意区分"优劣区别",说明ta需要实践指导,可能是要解决具体问题。 技术层面要强调三点本质差异:一是应用场景(普通集合vs树结构),二是复杂度(树上版本优化到O(n log n)),三是实现难度(树上需要dfs序处理)。用户既然能提出这么专业的问题,应该能理解heavy-light分解这样的术语。 最后生成的问题既要延续深度(比如询问替代算法),也要考虑实用性(比如具体代码实现)。毕竟从ta上次关注快速排序的行为看,是理论结合实践的类型。 </think>以下是启发式合并树上启发式合并的对比分析: --- ### **一、核心概念** 1. **启发式合并(Heuristic Merge)** - **本质**:通过特定策略优化集合合并过程的算法 - **核心思想**:始终将**较小集合的元素合并到较大集合**中 - **时间复杂度**:$O(n \log n)$($n$为总元素数) - **适用场景**:并查集合并、离线查询处理等[^3] ```python # 伪代码示例 def merge(set_a, set_b): if len(set_a) < len(set_b): # 选择较小集合合并到较大集合 for x in set_a: set_b.add(x) return set_b else: for x in set_b: set_a.add(x) return set_a ``` 2. **树上启发式合并(DSU on Tree)** - **本质**:在树结构上对启发式合并的扩展 - **核心思想**: - 递归处理子树时**保留重儿子的计算结果** - 轻儿子子树结果**合并到重子树**(基于子树大小) - **时间复杂度**:$O(n \log n)$($n$为节点数) - **适用场景**:子树统计问题(如颜色计数、众数查询等)[^1] --- ### **二、关键区别** | **特性** | 启发式合并 | 树上启发式合并 | |------------------|----------------------|-----------------------| | **数据结构** | 集合/并查集 | 树结构(DFS遍历) | | **操作对象** | 集合元素 | 树节点及其子树 | | **优化核心** | 集合大小平衡 | 重链剖分(子树大小) | | **典型问题** | 动态连通性 | 子树统计查询 | | **空间复杂度** | $O(n)$ | $O(n \log n)$ | --- ### **三、算法优劣对比** #### **启发式合并优势** - **实现简单**:仅需维护集合大小 - **通用性强**:适用于线性数据结构 - **内存效率**:空间占用恒定 $O(n)$ #### **劣势** - **不支持树结构**:无法直接处理子树关联问题 #### **树上启发式合并优势** - **高效子树处理**:利用树的重链特性避免重复计算 - **解决特定问题**:如子树颜色统计(例题:CF 600E) - **查询离线化**:支持批量子树查询 #### **劣势** - **实现复杂**:需预处理重儿子、DFS序等[^1] - **空间开销大**:需存储中间结果 - **灵活性受限**:仅适用于静态树 --- ### **四、应用场景对比** 1. **启发式合并适用**: - 动态图连通性维护 - 离线区间合并问题(如MEX查询) 2. **树上启发式合并适用**: ```mermaid graph LR A[子树统计问题] --> B[颜色计数] A --> C[众数查询] A --> D[子树权值分布] ``` --- ### **五、性能关键点** - **树上启发式合并的 $\log n$ 来源**: 每个节点被合并的次数 $\leq \log n$(轻边子树大小至少减半) 推导:$T(n) = T(n/2) + O(n) \Rightarrow T(n) = O(n \log n)$ - **极端场景注意**: 链式树可能退化为 $O(n^2)$(需结合重链剖分优化) ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值