以一个例子开始吧,我觉得这样比较好理解。如果Marry和Tom是亲戚,Tom和Ben是亲戚,你可以推出Marry和Ben是亲戚。再者,{1,2}两个点是连通的,{2,3}两个点也是连通的,当出现{1,3}这对点的时候,三个点就组成环了,这在Kruskal算法(最小生成树)中很有用。
我们使用并查集主要是为了寻找一个元素的代表,如下图所示,其中e就是集合{e,f,g}的代表,意思是,当进来一组新的集合,比如说是{g,a},这时要寻找第一个元素g的代表,那就是e了,然后寻找a的代表,如果二者代表一样,就是二者是亲戚。
根据上面所述的,并查集的基本操作分为:
1. makeSet(s):建立一个新的并查集,其中包含 s 个单元素集合。
2. unionSet(x, y):把元素 x 和元素 y 所在的集合合并,要求 x 和 y 所在的集合不相交,如果相交则不合并。
3. find(x):找到元素 x 所在的集合的代表,该操作也可以用于判断两个元素是否位于同一个集合,只要将它们各自的代表比较一下就可以了。
并查集是用树来表示集合的,但是这个树并没有表示为node*结点的形式,而是用两个数组来表示的,其中一个数组uset[]表示每个元素,rank[]则表示每个元素的秩,什么是秩呢?来看一下这用情况下图左边的这种情况:
当查找d的代表时,需要向上查找3次,也就是树的高度,这样很浪费时间,所以需要应用一种非常简单而有效的策略——路径压缩。路径压缩就是在每次查找时,令查找路径上的每个节点都直接指向根节点,对应图中右侧情况。将具有较小秩的树根指向具有较大秩的树根。简单的说,就是总是将比较矮的树作为子树,添加到较高的树中。也就是:
对应代码为:
const int MAXSIZE = 500;
int uset[MAXSIZE];
int rank[MAXSIZE];
void makeSet(int size) {
for(int i = 0;i < size;i++) uset[i] = -1;
for(int i = 0;i < size;i++) rank[i] = 0;
}
int find(int x) {
if (uset[x] < 0) return x;
uset[x] = find(uset[x]);
return uset[x];
}
void unionSet(int x, int y) {
if ((x = find(x)) == (y = find(y))) return;
if (rank[x] > rank[y]) uset[y] = x;
else {
uset[x] = y;
if (rank[x] == rank[y]) rank[y]++;
}
}