并查集
并查集(Disjoint-Set)是一种可以动态维护若干个不重叠的集合,并支持合并与查询的数据结构。详细地说,并查集包括如下两个基本操作:
1.Get,查询一个元素属于哪一个集合。
2.Merge,把两个集合合并成一个大集合。
为了具体实现并查集这种数据结构,我们首先需要定义集合的表示方法。在并查集中,我们采用“代表元”法,即为每个集合选择一个固定的元素,作为整个集合的“代表”。
其次,我们需要定义归属的表示法。第一种思路是维护一个数组f,用f[x]保存元素x所在集合的“代表”。这种表示方法可以快速查询元素的归属集合,但在合并时需要修改大量元素的f值,效率很低。第二种思路是使用一棵树形结构存储每个集合,树上的每个节点都是一个元素,树根是集合的代表元素。整个并查集实际上是一个森林。我们仍然可以维护一个数组fa来记录这下森林,fa[x]保存x的父节点。特别地,令树根的fa值为它自己。这样一来,在合并两个集合时,只需要连接两个树根(令其中一个树根为另一个树根的子节点,即fa[root1]=root2)。不过在查询元素的归属时,需要从该元素开始通过fa存储的值不断递归访问父节点,直至到达树根。为了提高查询效率,并查集引入了路径压缩与按秩合并两种思想。
路径压缩与按秩合并
第一种思想的查询效率很高,我们不妨考虑所这两种思路进行结合。实际上,我们只关心每个集合对应的“树形结构”的根节点是什么,并不关心这棵树的具体形态。
因此,我们可以在每次执行Get操作的同时,把访问过的每个节点(也就是所查询元素的全部祖先)都直接指向树根,这种优化方法被称为路径压缩。采用路径压缩优化的并查集,每次Get操作的均摊复杂度为O(logN)。
还有一种方法被称为按秩合并。所谓“秩”,一般有两种定义。有的资料把并查集中集合的“秩”定义为树的深度(不进行路径压缩)。有的资料把集合的“秩”定义为集合的大小。无论采取哪种定义,我们都可以把集合的“秩”记录在“代表元素”,也就是树根上。在合并时都把“秩”较小的树根作为“秩”较大的树根的子节点。
值得一提的是。当“秩”定义为集合的大小时,“按秩合并”也称为“启发式合并”,它是数据结构相关问题中一种重要的思想,应用非常广泛,不只局限于并查集中。启发式合并的原则是:把“小的结构”合并到“大的结构”中,并且只增加“小的结构”的查询代价。这样一来,把所有结构全部合并起来,增加的总代价不会超过NlogN。故单独采用“按秩合并”优化的并查集,每次Get操作的均摊复杂度也是O(logN)。
同时采用“路径压缩”和“按秩合并”优化的并查集,每次Get操作的均摊复杂度可以进一步降低到O(a(n)),其中a(n)为反阿克曼函数,它是一个比“对数函数”logN增长还要慢的函数,a(N)可近似为一个常数。
在实际应用中,我们一般只用路径压缩优化就足够。
具体代码
1.并查集的存储
使用一个数组fa保存父节点(根的父节点设为自己)
int fa[SIZE] //定义数组,SIZE为元素大小
2.并查集的初始化
设有n个元素,起初所有元素各自构成一个独立的集合,即有n棵1个点的树。
for(int i=1;i<=n;i++) fa[i]=i;//每个元素为一个树,树根是指向自己的
3.并查集的Get操作
若x是树根,则x就是集合代表,否则递归访问fa[x]直到根节点。
int get(int x) { if(x==fa[x]) return x;//如何查找到树根,返回代表元树根 return fa[x]=get(fa[x]);//路径压缩,fa直接赋值为代表元素 }
4.并查集的Merge操作
合并元素与元素y所在的集合,等价于让x的树根作为y的树根的子节点。
void merge(int x,int y) { fa[get(x)]=get(y); }