并查集维护邻边

在一些题目中需要不断地将一些点合并为一个点,同时也就带来了麻烦:如何高效地维护邻居集合?

我们可以将邻边启发式合并,其复杂度可以做到 O ( m log ⁡ n ) O(m \log n) O(mlogn)

具体来说,在访问时,从邻接表的末端向前扫描,如果边的两个端点已经在同一集合里,就将这条边删了。如此可以保证访问次数和边数基本相等。

在合并两个点集时,将边数小的点集合并到边数大的点集里去,可以证明,该复杂度为 O ( m log ⁡ n ) O(m \log n) O(mlogn)

几点注意

  1. 操作时,我们只需要考虑各个集合的根,也就是说所有的信息都是保存在该集合的根节点上的。
  2. 在并查集与优先队列一起用时,我们不能轻易的 s w a p swap swap节点,而是应该 s w a p swap swap信息。这是因为当你 s w a p swap swap节点时,优先队列里可能就出现的同一节点的新旧两个版本;而你 s w a p swap swap信息时,优先队列里每个根节点的信息都是唯一的。

例题:CF1515F Phoenix and Earthquake

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

#define rep(i, n) for (int i = 0; i < (int)(n); ++ i)
#define rep1(i, n) for (int i = 1; i <= (int)(n); ++ i)
#define foreach(itr, c) for (__typeof((c).begin()) itr = (c).begin(); itr != (c).end(); ++ itr)

typedef long long LL;
typedef pair<int, int> pii;

const int N = 3e5 + 5;

int n, m;
LL K, a[N], sum;
vector<pii> G[N];

struct DSU
{
    int fa[N], siz[N];
    void init(){ rep1(i, n) fa[i] = i, siz[i] = 1; }
    int find(int x){ return x == fa[x] ? x : fa[x] = find(fa[x]); }
    bool same(int x, int y){ return find(x) == find(y); }
    void merge(int x, int y)
    {
        x = find(x), y = find(y);
        if (x == y) return ;
        fa[y] = fa[x], siz[x] += siz[y];   // merge时不需要启发式
    }
} dsu;

int main()
{
    scanf("%d%d%lld", &n, &m, &K);
    rep1(i, n) scanf("%lld", &a[i]);

    rep1(i, m){
        int u, v; scanf("%d%d", &u, &v);
        G[u].push_back(MP(v, i)), G[v].push_back(MP(u, i));
    }
    rep1(i, n) sum += a[i];
    if (sum < 1ll * (n - 1) * K){
        printf("NO\n");
        return 0;
    }

    priority_queue<pair<LL, int> > pq;
    rep1(i, n) pq.push(MP(a[i], i));

    printf("YES\n");
    dsu.init();
    rep(cnt, n - 1){
        int u = pq.top().second; pq.pop();
        while (dsu.find(u) != u){                // 只需要考虑根节点
            u = pq.top().second;
            pq.pop();
        }
        while (dsu.same(G[u].back().first, u))   // 从后向前遍历邻接表
            G[u].pop_back();                     // 如果两端点在同一集合,就删除该边
        int v = dsu.find(G[u].back().first);
        printf("%d\n", G[u].back().second);

        dsu.merge(u, v);                         // 不改变根节点
        a[u] += a[v] - K;
        pq.push(MP(a[u], u));
        if (G[u].size() < G[v].size()) swap(G[u], G[v]);        // swap信息,非节点
        G[u].insert(G[u].end(), G[v].begin(), G[v].end());      // 合并边集
        G[v].clear();                   // 注意清空
    }

    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值