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

博客围绕一道有根树相关题目展开,题目要求计算以每个结点为根的子树中占主导地位的颜色编号和。解题思路是运用树上启发式合并(dsu on tree),介绍了树启的适用情况及流程,包括dfs记录重儿子、dsu遍历节点等步骤,最后给出代码。
题目描述:点击进入
题意

有一棵 n 个结点的以 1 号结点为根的有根树。
每个结点都有一个颜色,颜色是以编号表示的, i 号结点的颜色编号为 ci 。
如果一种颜色在以 x 为根的子树内出现次数最多,称其在以 x 为根的子树中占主导地位。显然,同一子树中可能有多种颜色占主导地位。
你的任务是对于每一个 i∈[1,n],求出以 i 为根的子树中,占主导地位的颜色的编号和。

思路

在刘爷跟杨大佬的催促下我终于开始了这个“人均 dsu ”的学习
(这意味着我是人均之下,刘爷还诱惑我学串串, “ 居心叵测 ” 啊/(ㄒoㄒ)/~~)
一道练习树上启发式合并(dsu on tree)的经典题目
树启简介:
1、只有对子树的询问;
2、没有修改;
流程:
一、dfs 记录每个点的重儿子
二、dsu 遍历每一个节点;
1、 递归解决所有的轻儿子,同时消除递归产生的影响;
2、递归重儿子,不消除递归的影响;
3、统计所有轻儿子对答案的影响;
4、更新该节点的答案;
5、删除所有轻儿子对答案的影响;

代码
#include<iostream>
#include<cstdio>
#include<vector>
#include<cstring>
#include<set>
#define int long long
using namespace std;
const int maxn=1e5+10;
typedef long long ll;
typedef unsigned long long ull; 
int n,tot; 
int ans[maxn];//记录每个节点子树的答案 
int sum[maxn];// 子树的大小 
int col[maxn];//节点的颜色 
int cnt[maxn];//某种颜色的数量 
int fson;//标记重儿子 
int son[maxn];//统计节点的重儿子
int maxx;//记录子树中最多的颜色的数量
int p;//记录子树中最多的颜色的节点编号的和。 
int head[maxn]; 
struct node 
{
	int to;
	int next;
}edge[maxn<<1];
void add(int u,int v)
{
	edge[tot].to=v; 
	edge[tot].next=head[u]; 
	head[u]=tot++;
}
void dfs(int pos,int fa)//查找重儿子
{
	sum[pos]=1;
	for(int i=head[pos];i!=-1;i=edge[i].next)
	{
		int v=edge[i].to;
		if(v==fa) continue;
		dfs(v,pos);
		sum[pos]+=sum[v];
		if(sum[son[pos]]<sum[v]) son[pos]=v;//更新重儿子 
	}
}
void change(int pos,int fa,int d) 
{
	cnt[col[pos]]+=d;//当前节点的颜色数量 + d 
	if(cnt[col[pos]]>maxx)//更新最大颜色的数量以及编号和 
	{
		maxx=cnt[col[pos]];
		p=col[pos];
	}
	else if(cnt[col[pos]]==maxx) p+=col[pos];//更新编号和 
	for(int i=head[pos];i!=-1;i=edge[i].next)// 
	{
		int v=edge[i].to;
		if(v==fa||v==fson/*绕过被标记的重儿子*/) continue;
		change(v,pos,d);
	}
}
void dsu(int pos,int fa,int keep)//keep:是否保留当前子树的数据
{
	for(int i=head[pos];i!=-1;i=edge[i].next)//遍历轻儿子,删除递归影响 
	{
		int v=edge[i].to;
		if(v==fa||v==son[pos]) continue;
		dsu(v,pos,0);
	}
	if(son[pos])//遍历重儿子,保留影响 
	{
		dsu(son[pos],pos,1);
		fson=son[pos]; //标记重儿子
	}
	change(pos,fa,1); //把轻儿子与重儿子的数据合并
	fson=0;//取消标记
	ans[pos]=p; //更新记录答案 
	if(!keep)//删除所有轻儿子对答案的影响 
	{
		change(pos,fa,-1);
		p=0;
		maxx=0;
	} 
}
signed main()
{
	memset(head,-1,sizeof(head));
    scanf("%lld",&n);
    for(int i=1;i<=n;i++) scanf("%lld",&col[i]);
    for(int i=1;i<n;i++)
    {
  	    int x,y;
  	    scanf("%lld%lld",&x,&y);
  	    add(x,y);
		add(y,x);
    }
    dfs(1,0); //从根节点开始
	dsu(1,0,1);
    for(int 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)$(需结合重链剖分优化) ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值