应用:union/set及其应用
并查集是一种树形的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题,常常在使用中以森林表示。
1. 等价关系
具有自反性、对称性和传递性三个性质称为等价关系,如电气连通性。
给定一个等价关系“~”,基于以上性质,为判断是否a~b,只需验证a和b是否同属一个等价类,这给我们提供了解决等价问题的方法。
2. 基本操作
在初始化时使每个元素都属于一个集合,集合间不相交(disjoint)。
find: 返回给定元素的集合(即等价类)的名字;
union:使用求并操作添加关系,通常是先使用find判断二者是否已经有关系。
解决动态等价问题的方案有两种:一种是保证指令find能够以O(1)运行;另一种是保证union能以O(1)运行。二者不可兼得。
3. 数据结构实现和分析
3.1 find的O(1)可行性
要保证find的O(1)性,union就必须多做一些工作。一种方案是在每次union时扫描待合并集合,将其类名j全换成目标集合类名i,这样连续N-1次union的复杂度为O(N*N);另一种想法是将每个等价类放入一个链表中,可以节省更新时间,但减少不了实际的渐进时间;还有一种是合并过程中,将较小类的名字改成较大的等价类名字,这样任意顺序的M次find和N-1次union最多花费O(M+NlogN)。
3.2 union的O(1)可行性
1)naive结构
直接将root2的根改为root1,单次find最坏是O(N)
class DisjSets { public: explicit DisjSets(int numElements); int find(int x) const; int find(int x); void unionSets(int root1, int root2); private: vector<int> s; }; //初始化所有元素为树根,分别代表一个单独的等价类 DisjSets::DisjSet(int numElements): s(numElements){ for (int i = 0; i < s.size(); i++ ) s[i] = -1; } //rooti代表set_i的根,直接将rhs连到lhs的根上进行合并 void DisjSets::unionSets(int root1, int root2) { s[roo2] = root1; } //递归查找元素x的树根(即所属集合) int DisjSets::find(int x) const { if (s[x] <0) return x; else return find(s[x]); }2)按大小求并(union by size)
合并时总是使较小的树成为较大的树的子树,可以证明,任何节点的深度不会超过logN,单次find操作最坏为O(logN)。实现时通常让树根元素的值代表当前树的大小(树根都是负值)。
3)按高度求并(union by height)
合并时通过跟踪每棵树的高度(负值)使得浅树成为深度的子树,保证树的深度最多是O(logN) 。与按大小求并异曲同工。
//按高度求并 void DisjSets::unionSets(int root1, int root2) { if (-s[root1] < -s[root2]) //root2深于root1 s[root1] = root2; else { if (s[root1] == s[root2]) s[root1]--; //root1与root2高度相等,总体高度加1 s[root2] = root1; } }4)find的路径压缩(path compression)
基本上说,union操作的任何算法都会产生相同的最坏情形的树(即find的O(logN)。此时要降低整体的复杂度,就需要再次从find函数下手。
一种方案是进行路径压缩,其效果是:每执行一次find函数,从x到树根上的每一个节点都使它的父节点变成树根。
int DisjSets::find(int x) { if(s[x] < 0) return x; else return s[x] = find(s[x]); }需要注意的是,路径压缩与按大小求并完全兼容(因为压缩后没有改变树的大小);但不与按高度求并兼容(因为改变了高度),由于无法准确计算压缩后数的高度,通常对于每棵树存储的高度都是估计值(称为“秩”),按秩求并理论上和按大小求并的效率是一样的。
可以证明,使用按秩求并和路径压缩探测方法最坏情况下几乎是线性的,最坏情况下需要的时间是,实际中一般,一个较弱的结果是O(MlogN)。
4. 应用
迷宫生成算法
《数据结构与算法分析 C++描述》原书第8章整理