并查找数据结构

Union-find data structure

本文关键字:数据结构 查找      更新时间:2023-10-16

对于许多问题,我认为推荐的解决方案是使用并查找数据结构。我试着阅读它并思考它是如何实现的(使用c++)。我目前的理解是,它只不过是一个集合列表。为了找到一个元素属于哪个集合我们需要n*log n运算。当我们需要进行并并运算时,我们需要找到两个需要合并的集合然后对它们进行set_union运算。在我看来这不是很有效率。我对这个数据结构的理解是正确的,还是我遗漏了什么?

这是一个相当晚的回复,但这可能没有在stackoverflow的其他地方得到回答,并且由于这是搜索union-find的人的最热门页面,这里是详细的解决方案。

Find-Union是一个非常快的操作,几乎在常数时间内执行。它遵循Jeremie关于路径压缩和跟踪集大小的见解。对每个查找操作本身执行路径压缩,因此需要平摊lg*(n)时间。lg*就像逆阿克曼函数,增长非常缓慢,很少超过5(至少到n<2 ^ 65535)。联合/合并集是惰性执行的,只需将一个根指向另一个根,特别是将较小集合的根指向较大集合的根,这在常量时间内完成。

参考下面的代码https://github.com/kartikkukreja/blog-codes/blob/master/src/Union%20Find%20%28Disjoint%20Set%29%20Data%20Structure.cpp

class UF {
  int *id, cnt, *sz;
  public:
// Create an empty union find data structure with N isolated sets.
UF(int N) {
    cnt = N; id = new int[N]; sz = new int[N];
    for (int i = 0; i<N; i++)  id[i] = i, sz[i] = 1; }
~UF() { delete[] id; delete[] sz; }
// Return the id of component corresponding to object p.
int find(int p) {
    int root = p;
    while (root != id[root])    root = id[root];
    while (p != root) { int newp = id[p]; id[p] = root; p = newp; }
    return root;
}
// Replace sets containing x and y with their union.
void merge(int x, int y) {
    int i = find(x); int j = find(y); if (i == j) return;
    // make smaller root point to larger one
    if (sz[i] < sz[j]) { id[i] = j, sz[j] += sz[i]; }
    else { id[j] = i, sz[i] += sz[j]; }
    cnt--;
}
// Are objects x and y in the same set?
bool connected(int x, int y) { return find(x) == find(y); }
// Return the number of disjoint sets.
int count() { return cnt; }
};

该数据结构可以表示为树,其分支是反向的(分支不是向下指向,而是向上指向父节点——并将子节点与其父节点链接起来)。

如果我没记错的话,它可以(很容易地)显示:

  • 路径压缩(无论何时查找集合a的"父",您都要"压缩"路径,以便将来每次调用这些都将在O(1)时间内提供父)将导致每次调用O(log n)复杂度;

  • 这种平衡(你大致跟踪每个集合所拥有的子集合的数量,当你必须"联合"两个集合时,你让子集合最少的那个成为子集合最多的那个的子集合)也会导致每次调用的复杂度为O(log n)。

一个更复杂的证明可以表明,当你把两种优化结合起来时,你得到的平均复杂度是逆Ackermann函数,写为α(n),这是Tarjan对这个结构的主要发明。

我相信,后来证明,对于某些特定的使用模式,这种复杂性实际上是恒定的(尽管对于所有实际目的,ackermann的逆约为4)。根据维基百科关于Union-Find的页面,在1989年,任何等效数据结构的每次操作的平销成本显示为Ω(α(n)),证明当前的实现是渐近最优的。

合适的联合查找数据结构在每次查找过程中使用路径压缩。这就摊平了成本,然后每个操作都与阿克曼函数的逆成正比,这基本上使它成为常数(但不是完全)。

如果你是从头开始实现它,那么我建议使用基于树的方法。

一个简单的并集结构保持一个数组(element -> set),使得查找哪个集合是常量时间;更新它们的平摊时间是log n,连接列表的时间是常数。没有上面的一些方法那么快,但是对于编程来说很简单,并且足以改善Kruskal的最小生成树算法的Big-O运行时间。