在讨论不相交集数据结构之前,首先需要讨论等价关系。首先定义一下关系的概念:若对于每一对元素(a,b),a,bS,aRb或者为true或者为false,则称在集合S上定义关系R。如果aRb是true,那么a和b有关系。所谓的等价关系需要在关系的基础上满足下面三个性质:
1.(自反性)对于所有的aS,aRa。
2.(对称性)aRb当且仅当bRa。
3.(传递性)若aRb且bRc,则aRc。
基于上面的定义可知,相等关系是一个等价关系,小于等于或者大于等于不是等价关系,因为不满足对称性。
在等价关系基础之上,继续讨论动态等价性问题。假设给定属于同一个集合的两个元素a和b,需要判定这两个元素是否等价。一种思路是将等价关系存储为一个二维布尔数组,这样判断两个元素是否等价的操作可以在常数时间内完成。但是通常而言等价关系是不明显或者相当隐秘的,无法做到这么直观。为了解决这个问题,给出一个等价类的概念:一个元素aS的等价类是S的一个子集,它包含所有与a有关系的元素。等价类形成对S的一个划分,S中的每一个成员恰好出现在一个等价类中。为了确定a和b是否有等价关系,只需要验证a,b是否在同一个等价类中即可。
在动态等价性问题中,初始的输入数据是n个集合的类,每个集合含有一个元素。在这些等价类上可以执行的操作是Find和Union操作。 其中Find操作返回包含给定元素的集合的名字。Union操作是用于添加关系的,它将不同的等价类(集合)合并到一起,并且删除被合并的两个原来的等价类。在动态等价问题的实现中,有两种思路,一种是使Find操作花费较少的时间,另一种是使Union操作花费较少的时间。下面描述的实现采用Union操作较为简单,Find操作要难一些。
基本数据结构
这里的Find操作当操作于属于同一个不相交集合的两个元素上时,返回值应该是相等的。这里使用逻辑上的树结构来模拟这种操作,物理上使用数组来实现树这种逻辑结构,类似于堆的数组实现。选择树这种结构是因为属于同一颗树的元素有相同的根节点,在同一颗树上的节点上执行find操作可以返回树的根,这样就满足了不相交集的要求。当执行Union操作时,如果两个节点在同一个不相交集上,则不进行处理。如果两个节点不在同一个不相交集上,则合并两个不相交集。具体的操作是使一个根节点指向另一个根节点,即一棵树作为另一棵树的子树。初始情况下,每一个节点都是一棵树,这些树的集合为森林。使用数组数据结构来模拟树时,数组的索引就代表初始的数据元素(经过编号后的元素,每个索引代表一个元素)。数组在此索引处的值就代表其父节点,如果值为0,则这个索引代表的元素就是根节点。首先定义一下不相交集的类型声明:
#ifndef_DisjSet_H
typedef int DisJSet[NumSets+1];
typedef int SetType;
typedef int ElementType;
void Initilizalize(DisJSet S);
void SetUnion(DisJSet S,SetType Root1,SetType Root2);
SetType Find(ElementType X,DisJset S);
下面观察每个典型的方法,观察初始化例程:
void Initialize(DisJSet S)
{
int i;
for(i=NumSets;i>0;i--)
{
S[i]=0;
}
}
Find算法:(当索引值数组值为0是表明索引为根节点,返回,否则执行递归操作)
SetType Find(ElementType X,DisJSet S)
{
if(S[X]<=0)
return X;
else
return Find(S[X],S)
}
SetUnion(合并)算法:将一棵树作为另一棵树的子树
void SetUnion(DisJSet S,SetType Root1,SetType Root2)
{
S[Root2]=Root1;
}
灵巧求并算法
使用上面的Union操作是相当任意的,它通过使第二棵树成为第一棵树的子树而完成合并。这种合并方法没有考虑其他的因素,导致有可能出现深度很深的树。从而在执行Find操作时花费更多的时间。可以使用两种不同的求并操作来改进并集操作使得两棵子树合并时不造成深度过深的情况。可以保证O(logN)的深度,即最大深度不超过logN。这两种方法分别是按大小求并和按照高度(秩)求并集。在实现中,仍然使用一个数组来模拟不相交集合,但是在不相交集的元素中需要保存树的大小或者高度信息。这里采用的方式是在根处保存树的大小或者高度的负数,这样在每一次合并操作时,首先判断两棵树的大小或者高度,将其中较小或者高度较低的树作为另一个树的子树。需要注意的是,在合并两个树后,需要更新作为根节点的树的大小或者深度。下面是按高度(秩)求并的程序:
void SetUnion(DisJSet S,SetType Root1,SetType Root2)
{
if(S[Root2]<S[Root1])
S[Root1]=Root2;
Else
{
if(S[Root1]==S[Root2])
S[Root1]--;
S[Root2]=Root1;
}
}
路径压缩
在不相交集合上的一种改进是路径压缩操作,具体来说就是在每次执行Find操作时将从X到根的路径上的每一个节点都使它的父节点变成根。这样下次执行Find操作时可以减少向根处递归查找的次数。例程如下:
SetType Find(ElementType X,DisjSet S)
{
if(S[X]<=0)
return X;
else
return S[X]=Find(S[X],S);
}