9.6.1 ACM-ICPC 数据结构 并查集

9.6.1 ACM-ICPC 数据结构:并查集

并查集简介

并查集(Union-Find 或 Disjoint Set Union)是一种用于管理不相交集合的数据结构,主要支持两种操作:合并(Union)查找(Find)。它在解决连通性问题、图论问题(如最小生成树的 Kruskal 算法)以及其他需要动态连通性维护的场景中有着广泛应用。

并查集的核心思想是通过树结构表示集合中的元素,并通过路径压缩和按秩合并等优化手段提升操作效率。本文将介绍并查集的基本概念、操作及优化方法,并结合实际例子进行说明。

并查集的基本操作
  1. 查找(Find):查找元素所属的集合。通过在树结构中查找元素的根节点,我们可以确定该元素所属的集合。
  2. 合并(Union):将两个不同的集合合并为一个集合。该操作将两个树的根节点连接起来,形成一个新的树。
示例

假设有以下几个集合:

  • {1}
  • {2}
  • {3}
  • {4}

执行 Union(1, 2) 后,集合变为 {1, 2} 和 {3}, {4}。此时如果查询 Find(1)Find(2),返回的结果都应该是它们的公共根节点。

并查集的优化策略

在基本的并查集操作中,每次 Find 操作都需要遍历树,直到找到根节点,这在最坏情况下可能会退化为线性时间复杂度。为了提高效率,我们可以采用以下两种优化策略:

  1. 路径压缩(Path Compression): 在执行 Find 操作时,通过将路径上所有节点直接指向根节点,从而大幅减少后续操作的路径长度。路径压缩的作用是在进行查找时“压缩”树的高度,从而加速未来的查找操作。

  2. 按秩合并(Union by Rank): 在执行 Union 操作时,将较小的树合并到较大的树上,从而避免树的高度过大。这里的“秩”可以理解为树的高度或节点数量,通过按秩合并可以有效控制树的高度。

代码示例

以下是使用路径压缩和按秩合并优化的并查集实现:

class UnionFind {
public:
    vector<int> parent, rank;
    
    UnionFind(int n) {
        parent.resize(n);
        rank.resize(n, 0);
        for (int i = 0; i < n; i++) {
            parent[i] = i;
        }
    }
    
    int find(int u) {
        if (parent[u] != u) {
            parent[u] = find(parent[u]); // 路径压缩
        }
        return parent[u];
    }
    
    void unionSets(int u, int v) {
        int rootU = find(u);
        int rootV = find(v);
        if (rootU != rootV) {
            if (rank[rootU] > rank[rootV]) {
                parent[rootV] = rootU;
            } else if (rank[rootU] < rank[rootV]) {
                parent[rootU] = rootV;
            } else {
                parent[rootV] = rootU;
                rank[rootU]++;
            }
        }
    }
};
并查集的应用
  1. Kruskal 算法:并查集在 Kruskal 最小生成树算法中用于判断边的两个端点是否属于同一集合。每次加入一条边时,通过 Find 操作检查两个端点是否连通,如果不连通则通过 Union 操作合并两个集合。
  2. 动态连通性问题:在图论问题中,需要不断地查询和更新节点间的连通性,并查集可以高效地支持这一操作。
  3. 等价类问题:并查集可以用于处理等价类问题,将具有某种等价关系的元素划分为若干个不相交的集合。
个人见解与总结

并查集作为一种经典的数据结构,在解决图论和集合操作问题中有着不可替代的作用。通过路径压缩和按秩合并优化后的并查集,其时间复杂度接近于常数时间,能够高效地处理大量动态操作。对于学习算法竞赛或从事算法开发的人员来说,掌握并查集的实现与优化是非常必要的。

在实际应用中,并查集的概念非常直观,但要理解和实现其中的优化策略,需要对树结构的操作有较为深入的理解。通过反复练习和实际应用,可以更好地掌握这一强大的数据结构。


并查集:高级操作与应用

引入

并查集(Union-Find)是一种用于管理元素所属集合的数据结构,其核心思想是通过树形结构表示集合中的元素,并通过两种基本操作来维护集合的合并与查询:

  1. 合并(Union):合并两个元素所属的集合,将对应的树合并。
  2. 查询(Find):查询某个元素所属的集合,查找树的根节点。

并查集在很多算法竞赛和图论问题中都有广泛应用,特别是在处理连通性问题时极为高效。虽然并查集原本不支持低复杂度的集合分离操作,但通过一些扩展,可以实现如删除、移动元素等功能,甚至可以结合其他数据结构实现可持久化并查集。

并查集的初始化

初始状态下,每个元素都在一个独立的集合中,表示为一棵只有根节点的树。为了方便后续操作,我们通常将每个节点的父节点初始化为它自己。

struct dsu {
  vector<size_t> pa;

  explicit dsu(size_t size) : pa(size) {
    iota(pa.begin(), pa.end(), 0);  // 初始化每个元素为自身的父节点
  }
};
查询操作(Find)

Find 操作通过查找元素所属集合的根节点来实现。具体方法是不断访问当前节点的父节点,直到找到根节点。这个操作的实现很直观,但在深度较大的树中可能会导致效率低下。

size_t dsu::find(size_t x) {
  return pa[x] == x ? x : find(pa[x]);  // 递归查找根节点
}
路径压缩

路径压缩是一种优化策略。在执行 Find 操作时,将路径上所有访问到的节点直接连接到根节点,从而减少后续查询的路径长度。这种优化大大提升了查询效率,使得后续的 Find 操作接近常数时间复杂度。

size_t dsu::find(size_t x) {
  return pa[x] == x ? x : pa[x] = find(pa[x]);  // 路径压缩
}
合并操作(Union)

Union 操作将两个不同的集合合并为一个集合,具体实现为将两个集合的根节点相连。最简单的方式是直接将一个集合的根节点指向另一个集合的根节点。

void dsu::unite(size_t x, size_t y) {
  pa[find(x)] = find(y);  // 合并两个集合
}
启发式合并(Union by Rank)

合并操作时选择哪棵树的根节点作为新树的根节点,直接影响未来操作的效率。启发式合并策略建议将节点较少或深度较小的树合并到另一棵树,以减少树的高度。这种策略称为“按秩合并”,有效防止树的退化。

struct dsu {
  vector<size_t> pa, size;

  explicit dsu(size_t size_) : pa(size_), size(size_, 1) {
    iota(pa.begin(), pa.end(), 0);  // 初始化父节点和每棵树的大小
  }

  void unite(size_t x, size_t y) {
    x = find(x), y = find(y);
    if (x == y) return;
    if (size[x] < size[y]) swap(x, y);
    pa[y] = x;
    size[x] += size[y];
  }
};
删除操作(Erase)

在某些场景下,我们需要从并查集中删除元素。删除操作可以通过将该节点的父节点设为它自己来实现。为了确保删除的元素是叶子节点,我们可以预先为每个节点制作副本,并将副本作为父节点。

struct dsu {
  vector<size_t> pa, size;

  explicit dsu(size_t size_) : pa(size_ * 2), size(size_ * 2, 1) {
    iota(pa.begin(), pa.begin() + size_, size_);  // 初始化副本
    iota(pa.begin() + size_, pa.end(), size_);
  }

  void erase(size_t x) {
    --size[find(x)];  // 更新集合大小
    pa[x] = x;        // 将删除的元素父节点设为自己
  }
};
移动操作(Move)

移动操作与删除操作类似,通过将元素的父节点设为目标集合的根节点,实现元素从一个集合移动到另一个集合。

void dsu::move(size_t x, size_t y) {
  auto fx = find(x), fy = find(y);
  if (fx == fy) return;  // 如果元素已经在目标集合,直接返回
  pa[x] = fy;  // 移动元素
  --size[fx], ++size[fy];  // 更新集合大小
}
并查集的复杂度

通过路径压缩和启发式合并的优化,并查集的每个操作的平均时间复杂度为 O(α(n))O(\alpha(n))O(α(n)),其中 α\alphaα 是阿克曼函数的反函数,它的增长极其缓慢,因此在实际应用中可以认为并查集的操作几乎是常数时间的。

带权并查集

除了基本的合并和查询操作,带权并查集还能在集合的边上定义权值,通过路径压缩时的运算来解决更多复杂的问题。例如,在经典的“食物链”问题中,我们可以在并查集上维护模 3 意义下的加法群。

例题:UVa11987 Almost Union-Find

在此题目中,我们需要实现类似并查集的数据结构,支持以下操作:

  1. 合并两个元素所属集合;
  2. 移动单个元素;
  3. 查询某个元素所属集合的大小及元素和。
#include <bits/stdc++.h>

using namespace std;

struct dsu {
  vector<size_t> pa, size, sum;

  explicit dsu(size_t size_)
      : pa(size_ * 2), size(size_ * 2, 1), sum(size_ * 2) {
    // size 与 sum 的前半段没有使用,简化下标计算
    iota(pa.begin(), pa.begin() + size_, size_);  // 初始化父节点
    iota(pa.begin() + size_, pa.end(), size_);    // 初始化父节点副本
    iota(sum.begin() + size_, sum.end(), 0);      // 初始化元素和
  }

  void unite(size_t x, size_t y) {
    x = find(x), y = find(y);
    if (x == y) return;  // 如果两个元素已经在同一集合,直接返回
    if (size[x] < size[y]) swap(x, y);  // 按大小合并,保持树平衡
    pa[y] = x;  // 合并
    size[x] += size[y];  // 更新合并后集合的大小
    sum[x] += sum[y];    // 更新合并后集合的元素和
  }

  void move(size_t x, size_t y) {
    auto fx = find(x), fy = find(y);
    if (fx == fy) return;  // 如果元素已经在目标集合,直接返回
    pa[x] = fy;  // 移动元素
    --size[fx], ++size[fy];  // 更新集合大小
    sum[fx] -= x, sum[fy] += x;  // 更新元素和
  }

  size_t find(size_t x) { 
    return pa[x] == x ? x : pa[x] = find(pa[x]);  // 路径压缩
  }
};

int main() {
  size_t n, m, op, x, y;
  while (cin >> n >> m) {
    dsu dsu(n + 1);  // 初始化并查集,元素范围是 1..n
    while (m--) {
      cin >> op;
      switch (op) {
        case 1:
          cin >> x >> y;
          dsu.unite(x, y);  // 合并两个元素所在的集合
          break;
        case 2:
          cin >> x >> y;
          dsu.move(x, y);  // 移动元素
          break;
        case 3:
          cin >> x;
          x = dsu.find(x);  // 查找元素所在的集合
          cout << dsu.size[x] << ' ' << dsu.sum[x] << '\n';  // 输出集合大小和元素和
          break;
        default:
          assert(false);  // 不可达代码,防止非法操作
      }
    }
  }
}
总结

并查集是一种高效且强大的数据结构,尤其在解决动态集合管理和连通性问题时极为出色。通过路径压缩和按秩合并的优化策略,使得并查集的操作非常高效。进一步扩展并查集,还可以实现删除、移动操作,以及带权并查集等高级功能。掌握并查集的使用和优化,是提升算法竞赛和实际编程能力的关键步骤。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

夏驰和徐策

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

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

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

打赏作者

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

抵扣说明:

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

余额充值