【转载】启发式合并
https://zhuanlan.zhihu.com/p/560661911
数据结构学习笔记(8) 启发式合并
启发式合并是用来解决子树中的统计问题。
在codeforces上叫做dsu on tree(树上启发式合并)。这里我们主要是来讲在树上进行启发式合并。
实际上之前我有讲过启发式合并严格鸽:启发式合并 看似暴力实则很快的算法
还有利用启发式合并的并查集严格鸽:ACM——可撤销并查集教程
但是没有讲过树上启发式合并。
我们一般需要维护一个 sub[u] ,表示以 u 为根的子树中的点。
下图中 sub[3] = [3,6,7,8,9]

但是如果暴力维护每个 sub[u] 肯定是会爆炸的。
但是
启发式合并就是在合并的时候将size小的那个集合合并到size大的那个集合里面。
比如[1,2,3] 和 [3,5,6,7] 合并,选择遍历前者来把元素放入后者。
void merge(vector<int>& a, vector<int>& b) { if (a.size() > b.size()) { for (int x : b)a.push_back(x); } else { for (int x : a)b.push_back(x); } }
初看上可能感觉这就是个暴力。但是我们分析一下每个元素被push_back()了多少次。
一个集合中的元素被放入另一个集合中会被push_back()一次。但是这个元素所在的集合的大小至少扩大了一倍。所以一个元素最多被push_back()了 O(log(N)) 次。
也就是用启发式合并,总的时间复杂度为 \rm O(nlogn) 。
对于 \rm sub[u] ,可以从其子节点 \rm v ,利用启发式合并进行转移。
这里考虑下实现,一般的做法是,我们开一个
vector<int>sub[N]
\rm mx_{son} 表示子树最大的子节点。
然后我们把其它的子节点中的 sub 都放到这个 mx_{son} 中。
然后把这个 mx_{son} 复制给 u ,但是因为有个复制操作,所以需要。
id[u] 表示 u 被映射到了哪个位置。
这样有以下代码
vector<int>sub[N]; void dfs(int u, int fa) { id[u] = ++tot; int mx_son = -1, mx_sz = 0; for (int v : g[u]) { if (v == fa)continue; dfs(v, u); if (sub[id[v]].size() > mx_sz) { mx_sz = sub[id[v]].size(); mx_son = v; } } if (mx_son != -1)id[u] = id[mx_son];//复制操作 for (int v : g[u]) { if (v == fa)continue; if (v == mx_son)continue; for (int son : sub[id[v]]) sub[id[u]].push_back(son); } sub[id[u]].push_back(u); }
当然优化复制操作可以用c++11的move。
其实这样就可以直接做题了
题目链接:
题意:

做法:
我们在记录子树出现了哪些节点之外,还需要记录每种颜色出现的次数。
所以我们直接套一个结构体
struct node { int mx_cnt = 0;//最多的出现次数 ll mx_sum = 0;//出现次数最多的颜色的编号和 map<int, int>cnt; vector<int>list; void add(int u) { cnt[c[u]]++; if (cnt[c[u]] > mx_cnt)mx_cnt = cnt[c[u]], mx_sum = c[u]; else if (cnt[c[u]] == mx_cnt)mx_sum += c[u]; list.push_back(u); } int size() { return list.size(); } }sub[N];
这里我们选择直接套一个map,复杂度为 \rm O(nlog^2n) , 10^5 的数据完全够用。

这样我们套一下上面的代码,就可以愉快的做出本题了。
code
const int N = 1e5 + 5; int n, c[N], id[N], tot = 0; struct node { int mx_cnt = 0;//最多的出现次数 ll mx_sum = 0;//出现次数最多的颜色的编号和 map<int, int>cnt; vector<int>list; void add(int u) { cnt[c[u]]++; if (cnt[c[u]] > mx_cnt)mx_cnt = cnt[c[u]], mx_sum = c[u]; else if (cnt[c[u]] == mx_cnt)mx_sum += c[u]; list.push_back(u); } int size() { return list.size(); } }sub[N]; ll ans[N]; vector<int>g[N]; void dfs(int u, int fa) { id[u] = ++tot; int mx_son = -1, mx_sz = 0; for (int v : g[u]) { if (v == fa)continue; dfs(v, u); if (sub[id[v]].size() > mx_sz) { mx_sz = sub[id[v]].size(); mx_son = v; } } if(mx_son!=-1)id[u] = id[mx_son]; for (int v : g[u]) { if (v == fa)continue; if (v == mx_son)continue; for (int son : sub[id[v]].list) sub[id[u]].add(son); } sub[id[u]].add(u); ans[u] = sub[id[u]].mx_sum; } void slove() { cin >> n; for (int i = 1; i <= n; i++)cin >> c[i]; for (int i = 1; i <= n - 1; i++) { int u, v; cin >> u >> v; g[u].push_back(v); g[v].push_back(u); } dfs(1, 0); for (int i = 1; i <= n; i++)cout << ans[i] << " "; cout << endl; }
不过这个是一个比较裸的启发式合并的题目,大家可以做做下面两道题目练习。
严格鸽:Codeforces Round #760 (Div. 3) G(离线/并查集/数据结构)
严格鸽:Educational Codeforces Round 132 C(贪心) D E(启发式合并 + 懒标记)
除了启发式合并,我们还可以用线段树合并来解决此类问题。
本文作者:辜铜星
本文链接:https://www.cnblogs.com/gutongxing/p/18386984
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现