当前位置: 首页 > news >正文

【树论】树上启发式合并

树上启发式合并(DSU on Tree)详细讲解


一、引言:什么是树上启发式合并?

树上启发式合并(DSU on Tree,即 Disjoint Set Union on Tree),也称 Sack轻重链启发式合并,是一种用于高效处理离线树上子树查询的技巧。

它能够在 O(n log n) 的时间复杂度内,解决许多“对每个节点,统计其子树中满足某种条件的节点数量”的问题。


二、核心思想

2.1 朴素做法的问题

假设我们有一个问题:

对每个节点 u,求出以 u 为根的子树中,不同颜色的节点个数。

最直接的做法是:

  • 对每个节点 u,DFS 遍历其子树,用一个集合(如 setmap)记录所有出现的颜色。
  • 时间复杂度为 O(n^2 log n),对于 n = 10^5 来说不可接受。

2.2 启发式合并的核心思想

我们观察到:子树之间是独立的,可以合并信息。

但如果我们每次都新建一个数据结构来统计子树信息,代价太高。

启发式合并的关键是:

保留重儿子的信息,只合并轻儿子的信息。

因为从任意节点到根的路径上,最多只有 O(log n) 条轻边,所以每个节点最多被“合并” O(log n) 次。

从而总复杂度为 O(n log n)


三、前置知识:重链剖分(轻重儿子)

3.1 定义

  • 子树大小(size):以 u 为根的子树包含的节点数。
  • 重儿子(Heavy Child):子树大小最大的儿子。
  • 轻儿子(Light Child):其他儿子。
  • 重链:由重儿子连接形成的链。
void dfs_size(int u, int parent) {sz[u] = 1;heavy[u] = -1;  // 没有重儿子for (int v : adj[u]) {if (v == parent) continue;dfs_size(v, u);sz[u] += sz[v];if (heavy[u] == -1 || sz[v] > sz[heavy[u]])heavy[u] = v;}
}

四、算法流程(DSU on Tree)

4.1 步骤概览

对每个节点 u 执行以下操作:

  1. 递归处理所有轻儿子,并清除它们留下的信息。
  2. 递归处理重儿子,并保留它的信息。
  3. 将轻儿子的信息合并到当前节点
  4. 加入当前节点本身的信息
  5. 回答关于 u 子树的查询

4.2 关键点

  • 只有重儿子的信息被保留,其余都临时计算后合并。
  • 使用一个全局数据结构(如数组 cnt[])来维护当前正在统计的信息。
  • 在处理轻儿子时,要记得在 DFS 返回后清空其贡献。

五、C++ 实现模板

我们以经典问题为例:

对每个节点 u,求其子树中出现次数最多的颜色的出现次数(或所有颜色的频次统计)。

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;typedef long long ll;const int MAXN = 100010;vector<int> adj[MAXN];
int color[MAXN];
int sz[MAXN];         // 子树大小
int heavy[MAXN];      // 重儿子
ll cnt[MAXN];         // cnt[c] = 颜色c的出现次数(全局统计)
ll max_cnt = 0;       // 当前最大频次
ll sum = 0;           // 当前最大频次的颜色的权值和(可选)
int n;// 第一步:计算子树大小和重儿子
void dfs_size(int u, int par) {sz[u] = 1;heavy[u] = -1;for (int v : adj[u]) {if (v == par) continue;dfs_size(v, u);sz[u] += sz[v];if (heavy[u] == -1 || sz[v] > sz[heavy[u]])heavy[u] = v;}
}// 清除子树贡献
void add(int u, int par, int delta) {// delta = 1 表示添加,-1 表示删除cnt[color[u]] += delta;// 维护最大频次(可选)if (delta == 1) {if (cnt[color[u]] > max_cnt) {max_cnt = cnt[color[u]];sum = color[u];} else if (cnt[color[u]] == max_cnt) {sum += color[u];}} else if (delta == -1) {if (cnt[color[u]] + 1 == max_cnt) {if (color[u] == sum) {// 需要重新计算 max_cnt 和 sum// 但这里我们只在主函数中统一处理}}}for (int v : adj[u]) {if (v == par || v == heavy[u]) continue; // 跳过父节点和重儿子(在主流程中处理)add(v, u, delta);}
}// 主函数:DSU on Tree
void dfs(int u, int par, bool keep) {// 1. 先处理所有轻儿子,并清除它们的贡献for (int v : adj[u]) {if (v == par || v == heavy[u]) continue;dfs(v, u, false);  // 不保留信息}// 2. 处理重儿子,保留其贡献if (heavy[u] != -1) {dfs(heavy[u], u, true);}// 3. 将轻儿子的信息加进来for (int v : adj[u]) {if (v == par || v == heavy[u]) continue;add(v, u, 1);}// 4. 加入当前节点cnt[color[u]]++;if (cnt[color[u]] > max_cnt) {max_cnt = cnt[color[u]];sum = color[u];} else if (cnt[color[u]] == max_cnt) {sum += color[u];}// 5. 此时 cnt[] 中保存的是 u 子树的完整信息// 可以回答查询:比如 ans[u] = max_cnt 或 sumcout << "Node " << u << ": max frequency = " << max_cnt << ", sum of max-colors = " << sum << endl;// 6. 如果不需要保留信息(即当前是轻儿子),则清空整个子树贡献if (!keep) {add(u, par, -1);  // 删除整个子树的贡献max_cnt = 0;sum = 0;}
}

六、完整测试代码

int main() {cin >> n;for (int i = 1; i <= n; i++) {cin >> color[i];}for (int i = 1; i < n; i++) {int u, v;cin >> u >> v;adj[u].push_back(v);adj[v].push_back(u);}dfs_size(1, -1);dfs(1, -1, false);  // 最终根节点也不保留(可设true,但无影响)return 0;
}

✅ 这是均摊分析的结果。


七、支持的操作类型

DSU on Tree 适用于以下类型的子树查询:

问题类型是否支持
子树中不同颜色数量
某颜色出现次数
出现频率最高的颜色
子树中众数(mode)
某值是否在子树中出现
子树中满足某条件的节点数

❌ 不适用于:

  • 需要支持修改的在线查询(可用树链剖分 + 线段树)
  • 路径查询(非子树)
  • 动态树

八、优化技巧

8.1 使用 vector 替代 map

如果颜色值域大但实际使用少,可以用 map,但通常颜色编号可离散化。

// 离散化颜色
vector<int> colors;
// ... 收集所有 color[i]
sort(colors.begin(), colors.end());
colors.erase(unique(colors.begin(), colors.end()), colors.end());
for (int i = 1; i <= n; i++) {color[i] = lower_bound(colors.begin(), colors.end(), color[i]) - colors.begin();
}

然后 cnt 数组大小为 colors.size()

8.2 避免递归清除时栈溢出

对于深树,add() 函数递归可能爆栈。可改用栈或 BFS。


九、扩展应用

9.1 查询子树中出现至少 k 次的颜色数量

只需维护 freq[cnt] 数组,表示频次为 cnt 的颜色有多少种。

9.2 子树中众数的最小编号

add 函数中额外记录。

9.3 多种属性联合查询

如:颜色 + 深度,可用 map<pair<int,int>, int>,但注意复杂度。


十、常见错误与调试建议

错误说明
忘记跳过父节点导致死循环
忘记跳过重儿子重复添加
keep=false 时未清除内存污染
cnt[] 数组未清零多组数据出错
颜色未离散化数组越界
http://www.dtcms.com/a/356972.html

相关文章:

  • ansible的playbook练习题
  • 短剧小程序系统开发:助力影视行业数字化转型
  • 算法---字符串
  • Speculation Rules API
  • PDF转图片工具实现
  • 天气查询系统
  • 2025_WSL2_Ubuntu20.04_C++20_concept 环境配置
  • el-select多选下拉框出现了e611
  • MySQL 中ORDER BY排序规则
  • 物联网平台中的Swagger(二)安全认证与生产实践
  • Socket编程核心API与结构解析
  • 【C++】掌握类模板:多参数实战技巧
  • 构筑沉浸式3D世界:渲染、资源与体验的协同之道
  • 云计算学习笔记——逻辑卷管理、进程管理、用户提权RAID篇
  • N32G43x Flash 驱动移植与封装实践
  • DBeaver 的 PostgreSQL 驱动包默认存储位置
  • 序列化和反序列的学习
  • 移动社交时代电商流量获取新路径:基于社群与开源AI智能名片链动2+1模式S2B2C商城小程序的探索
  • 【基础-单选】关于Button组件,下面哪个样式是胶囊型按钮
  • 大模型之RAG, 检索增强生成
  • 【若依】RuoYi-Vue-springboot3分离版
  • RS485、RS232、RS422协议
  • 浔川代码编辑器v2.1.0公测版上线时间公告
  • 基于FPGA的DDR3读写实验学习
  • LeetCode算法日记 - Day 26: 归并排序、交易逆序对的总数
  • 河南葱香鸡蛋,嫩滑香浓超棒!
  • 企业微信如何设置长期有效的获客二维码?3步生成!
  • 机器人视觉检测
  • 决胜财报季,证券投研如何实现财报自动化分析录入?
  • 企微智能表格对接业务系统