「解题报告」CF1305G Kuroni and Antihype

首先抽象一下问题,发现实际上题目要求是找一个生成内向森林,定义权值为每条边指向的点的点权和,求最大权值。

首先考虑建一个虚点 \(0\),让森林中每棵树的树根指向它,这样就变成了一棵生成树。

发现每个点都会作为出边恰好一次,那么我们可以将 \(u\longleftrightarrow v\) 的边权设为 \(u + v\),这样权值就等于边权之和减点权之和,那么我们要求的问题就转化成了求最大生成树。

但是此时边数是 \(O(n^2)\) 的,直接求肯定爆炸。考虑模拟最大生成树的过程。

模拟 Kruskal

我们要求将边权从大往小排序,依次加边。虽然边很多,但是边权数量是很小的,所以考虑直接从大往小枚举边权。又考虑到两个点之间有边当且仅当没有交,那么枚举完边权之后,点权一定是边权的子集,于是我们可以枚举一步子集,然后并查集维护即可,复杂度 \(O(3^{18} \alpha(n))\)

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 300005;
int n;
int a[MAXN];
long long ans;
int cnt[MAXN];
int fa[MAXN];
int find(int x) {
    return fa[x] == x ? x : fa[x] = find(fa[x]);
}
void merge(int x, int y, int w) {
    if (cnt[x] && cnt[y]) {
        if (find(x) != find(y)) {
            x = find(x), y = find(y);
            ans += 1ll * w * (cnt[x] + cnt[y] - 1);
            fa[x] = y, cnt[y] = 1;
        }
    }
}
int main() {
    scanf("%d", &n);
    cnt[0] = 1;
    for (int i = 1; i <= n; i++) {
        scanf("%d", &a[i]);
        cnt[a[i]]++;
        ans -= a[i];
    }
    int V = 262144;
    for (int i = 0; i < V; i++) {
        fa[i] = i;
    }
    for (int i = V - 1; i >= 0; i--) {
        for (int j = i; j; j = (j - 1) & i) {
            merge(j, i - j, i);
        }
    }
    printf("%lld\n", ans);
    return 0;
}

模拟 Boruvka

还有一种最大生成树算法叫 Boruvka 算法,基本思想就是每次找到每个连通块的最大出边,然后将这样的最大边连接的连通块联通起来。这样每次连通块数量减半,一共只会经过 \(O(\log n)\) 次,则复杂度为 \(O(m \log n)\)

这个算法的好处是,每次找最大出边的过程是很灵活的。我们考虑放到这个题上来,我们需要找的其实就是对于每一个数 \(a_i\),找出 \(a_i \operatorname{and} a_j = 0\) 且不在同一连通块内的 \(a_j\) 中的最大值。这个东西可以使用类 FWT 的方法去转移。考虑同一连通块的限制,其实我们只需要额外再维护一个次大值,满足最大值与次大值不在同一个连通块内,那么就一定能够找到答案了。

这样复杂度为 \(O(V \log V \log n)\) 的,是很优秀的。

没写代码。

posted @ 2023-05-11 07:43  APJifengc  阅读(48)  评论(0编辑  收藏  举报