Skip to content

CF888G Xor-MST

原题链接:G. Xor-MST

Tag:图论、MST、Trie

题目描述

给定一个具有 \(n\) 个结点的完全无向图, 每个点给定点权 \(a_i\), 连接 \(i\)\(j\) 的代价为 \(a_i \oplus a_j\)

求这个图的最小生成树的权值。

数据说明: \(1 \leq n \leq 2 \times 10^5,0 \leq a_i \lt 2^{30}。\)

分析

解法1

首先看到题要求 \(\text{MST}\), 可以想到或许要使用一种 \(\text{MST}\) 的算法。

之后看数据范围,发现范围是 \(2e5\) 的, 并且题目给出的图是一个完全图, 结合边的代价为 \(a_i \oplus a_j\) 这一点, 如果我们在求 \(\text{MST}\) 的时候遍历了所有的边, 复杂度显然是不可接受的。 所以我们需要使用一种方法来优化选边的过程, 发现求最小代价的边的本质是一种最小异或对, 因此我们可以使用 \(\text{01Trie}\) 来寻找可以连接的边。

我们考虑一下找边时候的异或计算, 异或的本质是二进制下不进位的加法, 因此我们可以把位拆开考虑。 根据异或运算, 尝试把每一位按照是 \(0\) 还是 \(1\) 分成两组, 会发现这一位在 \(0\)\(1\) 两组内分别内部连边无疑是最优的, 因为这一位上没有产出额外的代价。 对于两边的点我们可以继续看更低位, 再尝试把更低位按照是 \(0\) 还是 \(1\) 分成两组 \(\cdots\)

对于上述这一段, 必须要说清楚的是, 这里的按照是 \(0\) 还是 \(1\) 分组是对于递归到的区间来说的, 递归到的区间保证了这些数字的高一位到最高位完全相同。

不难发现从这里开始我们进入了一个递归的过程, 我们考虑按位分组递归。 「递」的过程是我们把每一位按照是 \(0\) 还是 \(1\) 分成两组, 我们考虑一下「归」的时候要怎么进行左右两组的连边, 其实我们在「归」的时候左部分和右部分他们之间已经分别组成了连通块, 所以我们只需要找一条边让当前的左部分和右部分连起来就可以了, 因此我们这里在实现的时候可以把左部分插入到 \(\text{01Trie}\) 上, 之后在右部分遍历在 \(\text{01Trie}\) 上去查找最小的异或对就可以了。

这里的本质十分接近归并。

上述做法在递归时因有 \(logV\) 的复杂度因为有 \(logV\) 层, 每一层对于 \(\text{01Trie}\) 的处理是 \(nlogV\)的, 因此复杂度为 \(O(n\log^{2}V)\)

除此之外,上述做法的本质其实是类 \(\text{Kruskal}\), 因为我们保证了连边的时候从消耗最小的边开始连。

代码实现

解法1

struct Trie01 {
    static const int StrMaxLen = 2e5 + 5;//数字个数
    static const int SigmaSize = 31;//int开31 ll开63
    int trie[StrMaxLen * SigmaSize][2], idx = 0;
    void init(){
        for (int i = 0; i <= idx ; ++i) {
            trie[i][0] = trie[i][1] = 0;
        }
        idx = 0;
    }
    void insert(int x) {
        int cur = 0;
        for (int i = 30; i >= 0; i--) {
            int j = x >> i & 1;
            if (!trie[cur][j]) trie[cur][j] = ++idx;
            cur = trie[cur][j];
        }
    }
    int queryMinXorWithX(int x){
        int cur = 0, ans = 0;
        for (int i = 30; i >= 0; i--) {
            int j = x >> i & 1;
            if (trie[cur][j]) {
                cur = trie[cur][j];
            } else {
                ans += 1 << i;
                cur = trie[cur][!j];
            }
        }
        return ans;
    }
};
Trie01 trie;
void NeverSayNever() {
    int n;
    cin >> n;
    vector<int> val(n + 1);
    for (int i = 1; i <= n; ++i) {
        cin >> val[i];
    }
    std::sort(val.begin(), val.end());
    auto dfs = [&](auto &&self, int l, int r, int dep) -> int {
        if (dep < 0 || l >= r) return 0;
        int mid = l - 1;
        while (mid + 1 <= r && (val[mid + 1] >> dep & 1) == 0) {
            mid++;
        }
        if (mid == l - 1 || mid == r) {
            return self(self, l, r, dep - 1);
        }
        int ans = LLONG_MAX;
        trie.init();
        for (int i = l; i <= mid; ++i) {
            trie.insert(val[i]);
        }
        for (int i = mid + 1; i <= r; ++i) {
            ans = min(ans, trie.queryMinXorWithX(val[i]));
        }
        ans += self(self, l, mid, dep - 1) + self(self, mid + 1, r, dep - 1);
        return ans;
    };
    cout << dfs(dfs, 1, n, 30) << endl;
}

日志

本页面创建于 2024/09/01 13:24

特别鸣谢:

Ackerlanna的耐心指点。

UPD-1:2024/09/01 13:36

Jlyfish的指正下更正若干笔误。