最近很忙还接了一个“家教”单,又不想太糊弄,在备课的时候备课到并查集的部分,想起以前很难理解的并查集算法,就写了这篇博客。
并查集
1 待解决的问题
- 合并(Union):把两个不相交的集合合并为一个集合。
- 查询(Find):查询两个元素是否在同一个集合中。
2 与“树”章节的关联
树 --父节点表示法
在算法中数据结构与此类似,有些许不同的是,一般将根节点的父节点指向其自己,或者用
#define ROOT -1
然后根节点的父节点的索引号为ROOT
3 书上的描述
书上给出了一些等价对,然后让你判断某两个元素是否属于同一个集合
理解了这个问题需求,其实你也可以猜到,树的样子并不是那么重要,因此等价对处理的先后可能会造成画出来的树不一样,这是很正常的。
如何理解这类问题?
洛谷上有一道题,如下
某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。
通过这个问题就很好了解了,原来是来找同门的啊!
4. 求解
理解了问题和问题的需求之后,算法思想如下:
-
一开始每个元素都在独立的只包含一个结点的树中,而他自己就是根节点。
-
通过使用函数differ可以检查一个等价对中的两个元素是否在同一棵树中。
-
如果不是就通过union函数归并两个等价类。归并时通常让节点个数少的指向结点个数多的。
那么differ函数与union函数实现?
bool ParPtrTree::differ(int a, int b) {
int root1 =FIND(a); // Find root ofnode a.找a的根
int root2 =FIND(b); // Find root of node b.找b的根
return root1!= root2; // Compare roots.比较两根
}
void ParPtrTree::UNION(int a, int b) { // Merge subtrees.合并树
int root1 =FIND(a); // Find root of node a
int root2 =FIND(b); // Find root of node b
if (root1 !=root2) array[root2] = root1; // Merge合并
}
// FIND with path compression 返回根的值
int ParPtrTree::FIND(int curr) const {
if(array[curr] == ROOT) return curr; // At root已经是根则返回
array[curr] =FIND(array[curr]);
return array[curr];
}
理解算法原理后,做应用就不难了
我们再将他与父指针表示法与其相结合
例如这张图里,如果此时输入2 6 ,那么应该是拿1 4去比较谁的子节点多 ,“输家”会指向“赢家”
1 4之间的battle,会把1作为4的子节点插入,或者4作为1的子节点插入,而非直接对2 6进行操作。
5. 路径压缩
这也是一个小考点
比如这颗树,其实我们要判断的是这些节点是否是属于同一个集合,如果用这种方式,可能出现极端情况,因为在建树的过程中,树的最终形态严重依赖于输入数据本身的性质,比如数据是否排序,是否随机分布等等。比如在输入数据是有序的情况下,构造的BST会退化成一个链表。在我们这个问题中,也是会出现的极端情况的,比如这个H他的深度就很没有必要,首先做的一个改进是改进union的代码。
void union(int p, int q)
{
int i = find(p);
int j = find(q);
if (i == j) return;
// 将小树作为大树的子树
if (sz[i] < sz[j]) { id[i] = j; sz[j] += sz[i]; }
else { id[j] = i; sz[i] += sz[j]; }
count--;
}
可以发现,通过sz数组决定如何对两棵树进行合并之后,最后得到的树的高度大幅度减小了。这是十分有意义的,因为在Quick-Union算法中的任何操作,都不可避免的需要调用find方法,而该方法的执行效率依赖于树的高度。树的高度减小了,find方法的效率就增加了,从而也就增加了整个Quick-Union算法的效率。
那么find是否也可以做一个优化呢?
但是可以的
int find(int p)
{
//溯源
while (p != id[p])
{
id[p] = id[id[p]];
p = id[p];
}
return p;
}
//或者使用递归的方式
int find(int p)
{
if(id[p]==p) return p;
return id[p]=find(id[p]);
}
这棵树用之前的方法,查找(E,H) 会变成这样
而优化后的find使用后,会变成什么样呢?
习题练习
假设二叉树 BT 采用二叉链表存储结构,设计一个算法
void Parent(BinNode * BT,Elem x, BinNode *P)
求指定值为 x 的结点的父结点 P。提示:根结点的父为 NULL, 若在二叉树 BT 中未找到值为 x 的结点,P 也为 NULL