这篇文章主要讲解带权并查集的理论、设计和实践。
理论
并查集本质
这和以往的并查集模型不太一样。并查集的数据结构使用数组实现时,那么数据结构的本质的是一个含有多棵树的森林。下图是普通并查集的连接情况。
并查集连接方式
每一颗树本身代表其所有结点是在同一集合内,连接整个集合是通过数组的下标代表当前结点的序号,相应数组的值代表其父结点的序号的方式,这样的连接不带有其他关系
带权并查集
而在带权并查集是使得连接集合内的元素之间再添加一层关系,即两个元素之间还带有权值的意义。
从中我们不难发现普通并查集本质是不带权值的图,而带权并查集则是带权的图。
设计
普通并查集只使用数组id[MAXNUM]表示每个结点的父结点的情况,如果要设计带权并查集,显而易见我们需要另外构造一个数组来表示【权】,假设是R数组。
R数组是代表每个结点与其父结点的权值,也就是每个结点与其父结点的关系。
对于并查集的【并】和【查】操作来说,需要修改的部分:
【并】:并操作的实现有两种,一种是id[qRoot] = pRoot 直接将后面一个结点设置为前一个结点的父结点;另一种是按照树的秩大小决定合并,也叫Quick-Union 算法。
在【先后关系】的情况下,不能按照秩的大小进行合并。后者的算法效率明显优于前者。但是在路径压缩的前提下,后者的优化情况并没有这么明显。
许多文章都是采用前者的方法去做,但是经过不少实践(做题目)过后者的方法也是可以的,因为带权并查集从本质去看没有前后关系,下面会教大家如何在带权并查集下使用。
【查】:采用路径压缩,在将当前结点的父结点指向结点的父结点的父结点,也就是当前结点的父结点指向结点的爷爷结点,id[p] = id[id[p]],在指向之前将当前结点到父结点的权值和父结点到爷爷结点的权值进行处理,这个得具体根据题目决定。
应用
下面以POJ 1182 , HDU 3038这两道例题进行分析带权并查集的具体使用。
POJ 1182
【问题拆分】:假设题目所求的答案为ans。首先x,y不在1到N的范围内,则ans++。如果x,y都在1到N之间,则根据d的值进行相应的合并,合并时保持着关系(权值)。在合并之前进行检查,如果不符合前面的合并则ans++。
这里说到的关系需要利用一个r数组表示该结点与父节点的关系 ,其中r[i]=0代表同一类,r[i]=1代表被父节点吃,r[i]=2代表吃父节点。
刚才我们说过了带权并查集和普通并查集的区别就是在合并和查找(实际上是路径压缩)时对关系进行修改(改变r数组)。
首先我们看路径压缩的情况下,r数组有什么变化
【路径压缩】:路径压缩进行的操作是将当前结点的父结点指向结点的爷爷结点,这个时候r数组会发现什么变化呢,下图是路径压缩的某一个步骤。
从中可以看出y,z的关系没有改变。其中x指向了z,也就是id[x] = z;x和y的关系不存在,这个可以忽略,因为x和z建立新的关系,会覆盖x和y的关系。也就是r[x]的意义从表示x和y的关系变成x和z的关系。我们通过列出全部的情况进行判断x和z的变化情况。
(x, y) | (y, z) | (x,z) | 如何判断 |
---|---|---|---|
0 | 0 | 0 | 0+0 = 0 |
0 | 1 | 1 | 0+1 = 1 |
0 | 2 | 2 | 0+2 = 2 |
1 | 0 | 1 | 1+0 = 1 |
1 | 1 | 2 | 1+1 = 2 |
1 | 2 | 0 | (1+2)% 3 = 0 |
2 | 0 | 2 | 2+0 = 2 |
2 | 1 | 0 | (2+1)% 3 = 0 |
2 | 2 | 1 | (2+2)% 3 = 1 |
我们可以看出经过压缩后x和z的关系,即为r[z] = (r[x] + r[y]) % 3;也就是说在压缩前经历了上面的关系变化。所以路径压缩的代码如下:
// 查找父节点
int Find(int p) {
while (id[p] != id[id[p]]) {