等着阳光刺|

辜铜星

园龄:1年1个月粉丝:7关注:14

【转载】启发式合并

https://zhuanlan.zhihu.com/p/560661911

数据结构学习笔记(8) 启发式合并

启发式合并是用来解决子树中的统计问题

在codeforces上叫做dsu on tree(树上启发式合并)。这里我们主要是来讲在树上进行启发式合并。

实际上之前我有讲过启发式合并严格鸽:启发式合并 看似暴力实则很快的算法

还有利用启发式合并的并查集严格鸽:ACM——可撤销并查集教程

但是没有讲过树上启发式合并。

我们一般需要维护一个 sub[u]sub[u] ,表示以 uu 为根的子树中的点。

下图中 sub[3]=[3,6,7,8,9]sub[3] = [3,6,7,8,9]

但是如果暴力维护每个 sub[u]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))O(log(N)) 次。


也就是用启发式合并,总的时间复杂度O(nlogn)\rm O(nlogn)

对于 sub[u]\rm sub[u] ,可以从其子节点 v\rm v ,利用启发式合并进行转移。

这里考虑下实现,一般的做法是,我们开一个

vector<int>sub[N]

mxson\rm mx_{son} 表示子树最大的子节点。

然后我们把其它的子节点中的 subsub 都放到这个 mxsonmx_{son} 中。

然后把这个 mxsonmx_{son} 复制给 uu ,但是因为有个复制操作,所以需要。

id[u]id[u] 表示 uu 被映射到了哪个位置。

这样有以下代码

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。

其实这样就可以直接做题了

题目链接:

Problem - E - Codeforces

题意:

题意来源洛谷


做法:

我们在记录子树出现了哪些节点之外,还需要记录每种颜色出现的次数。

所以我们直接套一个结构体

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,复杂度为 O(nlog2n)\rm O(nlog^2n) , 10510^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 中国大陆许可协议进行许可。

posted @   辜铜星  阅读(4)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起