Educational Codeforces Round 32 G. Xor-MST(Brouvka最小生成树+0-1Trie合并)

原题链接:G. Xor-MST


题目大意:


给出一张 n n n 个点的完全图,每个点都有一个权值,点 i i i 和点 j j j 的边权为 a i a_i ai x o r xor xor a j a_j aj,其中 x o r xor xor 代表二进制异或操作。

问你这张图的最小生成树的权值是多少。

解题思路:


因为是完全图,因此边的总数为 n 2 n^2 n2 级别的,本题 n ≤ 2 × 1 0 5 n \leq 2\times 10^{5} n2×105 显然不能使用常规的最小生成树求法。

引入 B r o u v k a Brouvka Brouvka 最小生成树的思想:

  • 开始时枚举每一个点,对每个点枚举所有它的出边,找到一条价值最小的,且不与其属于同一连通块的边,将所有这样的边标记,若同一连通块内有多条这样的边则取最小值。
  • 对所有的边枚举完之后,将所有被标记边相连的两个点相连,合并成一个连通块,可知整张图的连通块数量至少会减半。
  • 重复上述步骤,直到图中只有一个连通块时停止,可以保证我们所得的一定是一颗最小生成树。

注意到每次连通块的数量至少会减半,那么算法最多会执行 log ⁡ n \log n logn 次,复杂度毫无疑问的是 O ( n log ⁡ n + m log ⁡ n ) O(n\log n+ m \log n) O(nlogn+mlogn) 的,其中 n n n 为点数, m m m 为边数。

那么问题来了,图中的边是 n 2 n^{2} n2 级别的,我们要去枚举所有边找到一条最小的边,显然不太可能。

注意到边权的计算为 a i a_i ai x o r xor xor a j a_j aj ,我们需要某种方式找到这类两两异或的最小值,显然我们可以用一颗 T r i e Trie Trie 贪心的解决。

那么我们要怎么快速的找到完全图的最小值呢?其实没有那么复杂,用类似主席树的思想即可。

假设你现在有一颗完全记录了 n = 8 n = 8 n=8 个点的 T r i e Trie Trie 树,你现在枚举的点为点 1 1 1,且其所在连通块内有 { 1 , 5 , 6 } \{1,5,6\} {1,5,6},那么你要怎么找到你和其他 { 2 , 3 , 4 , 7 , 8 } \{2,3,4,7,8\} {2,3,4,7,8} 的点,边权的最小值呢?

我们用大 T r i e Trie Trie 树减去我们当前 { 1 , 5 , 6 } \{1,5,6\} {1,5,6} 这个连通块的小 T r i e Trie Trie 树不就相当于从 n = 8 n=8 n=8 个点中将 { 1 , 5 , 6 } \{1,5,6\} {1,5,6} 这三个点挖去了吗?

T r i e { 2 , 3 , 4 , 7 , 8 } = T r i e { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 } − T r i e { 1 , 5 , 6 } \begin{align} Trie_{\{2,3,4,7,8\}} =& Trie_{\{1,2,3,4,5,6,7,8\}} - Trie_{\{1,5,6\}} \end{align} Trie{2,3,4,7,8}=Trie{1,2,3,4,5,6,7,8}Trie{1,5,6}

(这一步和主席树做差的思想一致,通过 [ 1 , r ] [1, r] [1,r] 的前缀信息与 [ 1 , l − 1 ] [1,l - 1] [1,l1] 的前缀信息相减即可得 [ l , r ] [l, r] [l,r] 的信息)

得到做差后的树后,我们就可将 a 1 a_1 a1 扔进这颗做差得到的 T r i e Trie Trie 贪心查询 x o r xor xor 最小值就可得边权最小的边了。

查询完之后,我们就获得了若干个信息,即 x x x 所在连通块与 y y y 所在连通块相连的边权最小,此时我们只需要将 x x x y y y T r i e Trie Trie 做一个类似线段树合并即可。

假设 a i a_i ai 值域为 [ 0 , V ] [0,V] [0,V] ,那么我们单次查询的复杂度为 O ( log ⁡ V ) O(\log V) O(logV),总复杂度为 O ( n log ⁡ V ) O(n \log V) O(nlogV) B r o u v k a Brouvka Brouvka 最多会执行 O ( log ⁡ n ) O(\log n) O(logn) 次, T r i e Trie Trie 合并均摊复杂度为 O ( n log ⁡ V ) O(n \log V) O(nlogV)

因此复杂度为 O ( n log ⁡ n log ⁡ V ) O(n \log n \log V) O(nlognlogV) ,可以通过。具体细节参考代码即可。


#include <bits/stdc++.h>
using namespace std;

using PII = pair<int, int>;
using i64 = long long;

const int N = 5e5 + 1, logn = 30;

//T为 Trie 树,cnt为该节点数量信息(cnt[x] - cnt[y] 即 x树 - y树的信息)
//leaf用于记录 ai 的下标,有多个相同的 ai 任取一个即可
int T[N * logn][2], cnt[N * logn], leaf[N * logn], idx;

//普通的插入初始化函数
void insert(int& R, int val, int id) {
    if (!R) R = ++idx;
    int cur = R, p = 0;
    for (int j = logn; j >= 0; --j) {
        p = val >> j & 1;
        ++cnt[cur];
        if (!T[cur][p]) {
            T[cur][p] = ++idx;
        }
        cur = T[cur][p];
    }
    ++cnt[cur];
    leaf[cur] = id;
}

//贪心查询最小值函数(返回最小值 aj 的下下标 j)
int Query(int pre, int cur, int val) {
    int p = 0;
    for (int j = logn; j >= 0; --j) {
        p = val >> j & 1;
        if (cnt[T[cur][p]] - cnt[T[pre][p]]) {
            cur = T[cur][p];
            pre = T[pre][p];
        } else {
            cur = T[cur][p ^ 1];
            pre = T[pre][p ^ 1];
        }
    }
    return leaf[cur];
}

//Trie 合并
int Merge(int pre, int cur) {
    if (!pre || !cur) {
        return pre | cur;
    }
    cnt[cur] += cnt[pre];
    T[cur][0] = Merge(T[pre][0], T[cur][0]);
    T[cur][1] = Merge(T[pre][1], T[cur][1]);
    return cur;
}

//并查集板子 注意merge函数
struct DSU {
    //ver[i] 代表点 i 所在并查集的 Trie树(注意 i 必须为并查集的根节点)
    vector<int> fa, siz, ver;
    DSU(int n) : ver(n + 1), fa(n + 1), siz(n + 1, 1) {
        iota(fa.begin(), fa.end(), 0);
    }
    int find(int x) {
        while (x != fa[x]) {
            x = fa[x] = fa[fa[x]];
        }
        return x;
    }
    int size(int x) { return siz[find(x)]; }
    bool same(int x, int y) { return find(x) == find(y); }
    bool merge(int x, int y) {
        x = find(x), y = find(y);
        if (x == y) return false;
        if (siz[x] > siz[y]) {
            swap(x, y);
        }
        //将 x -> y 的连通块合并时 同时将 x树 合并到 y树内
        ver[y] = Merge(ver[x], ver[y]);
        siz[y] += siz[x];
        fa[x] = y;
        return true;
    }
};

void solve() {
    int n;
    cin >> n;

    vector<int> a(n + 1);
    for (int i = 1; i <= n; ++i) {
        cin >> a[i];
    }

    //初始化并查集与 Trie
    DSU dsu(n);

    sort(a.begin(), a.end());
    for (int i = 1; i <= n; ++i) {
        insert(dsu.ver[0], a[i], i);
        insert(dsu.ver[i], a[i], i);
    }

    i64 ans = 0;

    //Brouvka迭代到并查集节点数为 n 即可停止
    while (dsu.size(1) != n) {
        //Edge[u]表示点u所在连通块最小边的信息,即 [a[u] ^ a[j]的边权,j的编号]
        vector<PII> Edge(n + 1, PII(INT32_MAX, 0));
        //枚举每个点 i 找最小值
        for (int i = 1; i <= n; ++i) {
            int u = dsu.find(i);//获得根节点的Trie树
            int v = Query(dsu.ver[u], dsu.ver[0], a[i]);//用大树和u树做差的同时查询最小值
            Edge[u] = min(Edge[u], PII(a[i] ^ a[v], v));//多条边中取最小即可
        }
        //将所有连通块合并
        for (int u = 1; u <= n; ++u) {
            if (dsu.find(u) != u) {
                continue;
            }
            auto& [d, v] = Edge[u];
            if (dsu.merge(u, v)) {
                ans += d;//加上权值
            }
        }
    }

    cout << ans << "\n";
}

signed main() {

    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);

    int t = 1; //cin >> t;
    while (t--) solve();

    return 0;
}

  • 23
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柠檬味的橙汁

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值