并查找数据结构
Union-find data structure
对于许多问题,我认为推荐的解决方案是使用并查找数据结构。我试着阅读它并思考它是如何实现的(使用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运行时间。
- 一种有效的数据结构,用于按 ID 访问和查找加权随机项
- 保持排序的数据结构,允许log N插入时间,并且可以返回我在log N中查找的元素的索引
- 用于添加和查找查询的适当数据结构
- 用于查找元素的数据结构
- C++ 数据结构队列:使用 for 循环查找队列中最大的元素
- 查找索引数据结构,例如`sTD :: vector`(非数组)
- 是否有具有对数时间插入、删除和查找(带距离)的排序数据结构
- 用于在具有更新查询的二维矩阵中查找最大值的最佳数据结构
- 在任意位置进行快速查找和删除的Deque样数据结构
- 最适合查找最小值的数据结构
- C++数据结构以查找多维数组中的相邻值
- 寻找快速初始化和快速查找的数据结构 (O(1))
- 尝试字典上的数据结构以查找押韵的单词
- 用于在数据结构中查找" "之间的字符串的代码
- 用于存储地址的单列列表的数据结构,更好地在C++中查找O(1)
- 最快的数据结构或算法,可快速查找 2 个键
- 什么是提供O(1)查找的C++数据结构
- 用于查找包含数字的非重叠范围的有效数据结构
- 并查找数据结构
- 支持表查找的数据结构