【转载】启发式合并

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(启发式合并 + 懒标记)



除了启发式合并,我们还可以用线段树合并来解决此类问题。

posted @ 2024-08-29 16:36  GuTongXing  阅读(28)  评论(0)    收藏  举报