Armin
迷茫的话 TRY AGAIN 多少次都能重新再来

并查集(union-find sets)

并查集所基于的数据结构是树形的数据结构,

整个树被抽象成为集合(Set),

而 树形结构中,节点被抽象为Elem,

树的根作为树中节点的祖先,

在树中,节点除了根节点以外都有父节点。

但是祖先节点只有一个。

即在集合Set中所有的元素Elem都有父节点,

但是它们的祖先节点有且只有一个:可以根据这一条件来判断两个元素

是否位于同一个集合中,即判断它们的祖先节点是否是相同的即可。

如果集合中只有一个节点那么,该集合的祖先节点为它本身。

 

基于该树形数据结构的主要三种原子操作有:

1.Make_Set(Elem x)

在刚开始接收元素的时候,每个元素之间都是相互孤立的集合。

这个操作用于把每一个元素初始化为一个仅包含一个元素的集合。

在初始化后每一个元素的父节点是它本身。

每个元素的祖先节点也是它本身。这里需要注意的是,父节点不一定是

祖先节点的。

也就是说

i-(root)>j->k

k的父节点是j,k的祖先节点是i

j的父节点是i,j的祖先节点仍然是i

 

2.Find_Set(Elem x)这个操作时查找一个元素所在的集合。

这个原子操作的本质思想就是找到元素x的祖先节点,

祖先节点对于一个集合来所是唯一的,

就像是对于一个树来所它的根可以

唯一确定一个树一样。

所以x元素的祖先节点所确定的集合一定是x所在的集合。

 

3.Union(Elem x, Elem y) 这个操作使用来合并元素x  和元素 y所在的集合的。

合并两个集合的思想是,

各自调用Find_Set(x) 和Find_Set(y)这两个操作,

找到两个元素所在集合的祖先,

然后根据树形结构的树的高度作为判断条件来

使得一个集合的祖先作为另一个集合的祖先。

这样就实现了合并两个元素所在的集合这一个操作。

 

即(x所在集合的祖先节点|变为子节点)->(y所在集合的祖先节点|仍旧是父节点&祖先节点)

或是(y所在集合的祖先节点|变为子节点)->(x所在集合的祖先节点|仍旧是父节点&祖先节点)

 

 

 

 并查集优化操作

1.递归查找祖先可以使得代码简洁易懂,但是如果集合对应的树形结构比较特殊的时候(例如每层仅有一个节点)

会消耗很多资源,效率也会下降,会使得最坏情况的时间空间复杂度大大加大。

所以可以在递归寻找祖先的时候,把最后一个节点的父节点设为父节点的父节点。

以此类推下去,会将树中所有节点都指向祖先节点,

这时候的树高为1(不算根节点的情况下):即所有的节点的父节点都是祖先节点了。

 

这就是所谓的路径压缩方法。

 

如果说并查集就不得不说并查集中的秩,

秩往往用来表示子树的深度或者是同一个集合里面的元素的个数。

2.而在合并集合的时候,通常是将元素少的集合合并到元素多的集合中,

这样合并后树的高度会相对较小。

 

下面是对于并查集比较经典的代码实现,

注释是LZ根据自己的理解增添上去的。

 

 

 

 1 int father[MAX];
 2 //这个数组是用来存放某一个元素的父节点元素值的
 3 //即,若设节点i的父节点是节点j的话,则由 father[i]=j;
 4 
 5 int rank[MAX];
 6 
 7 //这个数组是用来记录秩的,在本实现并查集的代码中
 8 //秩是用于记录子集中元素个数的
 9 //即 若元素节点i有k个子节点,那么就有 rank[i] = k;
10 
11 
12 //这个是初始化集合操作,在刚开始录入数据的时候,
13 //每个元素节点都是一个独立的集合,它们有0个子节点(因为自己无法是自身的子节点,
14 //但是自己却可以是自己的祖先节点 和 可以是自身的父亲节点)
15 
16 //但是具体的实现方法和初始化设定还需要根据具体情况进行推敲
17 
18 void Make_Set(int x)
19 {
20   
21   father[x] = x;//将自己设定为自己的父节点(祖先)
22   rank[x]  = 0; //集合中x的子集个数为0,故将其赋值为0       
23 }
24 
25 
26 
27 
28 //下面的代码在实现递归查找x元素所在集合的同时,
29 //又对代码进行优化处理,在回溯的时候,又对路径进行了相应的压缩
30 //其实就是,为了降低树的高度,将集合中的的x节点的父节点设成该集合的祖先节点(树的根)
31 //不仅仅如此,x父节点的父节点....一直到祖先节点,它们的父节点都是集合的祖先节点
32 //(其中祖先节点的父节点就是本身,所以  father[祖先]=祖先,循环会因为这个条件而停下来)
33 
34 int Find_Set(int x)
35 {
36     if(x != father[x])   //如果x的父节点不是它本身的话,x所在的集合中就一定存在其余元素节点
37     {
38        father[x]  = Find_Set(father[x]);
39 //继续寻找x父节点的父节点,一直会找到祖先节点
40 //一次类推的话,最后从x到x父节点...祖先节点,的父节点都会是祖先节点
41     }    
42 //循环会判断到father[x]=x这个地方停止,此时的x就是自身的父节点,即x就是祖先节点
43 
44     return father[x];  
45 //递归到x=father[x]停止,所以由此可知return 的father[x]即是
46 //在一开始传入操作的形参x所在集合的祖先节点
47 }
48 
49 
50 
51 
52 3.下面的这个操作是要合并传入形参x和y所在的集合
53 
54 void Union(int x , int y)
55 {
56     x = Find_Set(x);  //首先找到x所在集合的祖先节点
57     y = Find_Set(y);  //然后找到y所在集合的祖先节点
58    
59 
60 
61     if(x==y)   return;
62 //如果x、y值相等的话,说明 一开始传入形参的x y元素拥有同一个祖先,即x,y在相同的集合中
63   
64 
65 
66 //如果不相同的话,说明x、y在不同的集合中,可以合并
67 //根据Union的描述,首先比较x和y(ps:此时的x、y分别对应的是传入参数的相应的祖先节点
68 //所以rank[x]的值是 传入参数1 所在集合的元素总数
69 // rank[y]的值对应的是 传入参数2 所在集合的元素总数)
70 //将元素少的集合的祖先节点作为 元素多的集合的祖先节点的 子节点
71 //即,if对应的是将y所在的集合的祖先节点作为 x集合祖先节点的子节点
72 
73     if(rank[x]   >    rank[y])
74      {
75         father [y] = x;
76     }
77 
78 
79 
80 
81 //其余的情况,小于或等于都是y所在集合包含x所在的集合
82 
83     else
84     {
85         if(rank[x] == rank[y])
86         {
87               rank[y]++;
88          }
89 
90         father[x]=y;
91 
92     }
93 }

 

 

 

 

 

 

 

 

 

 

 

 

 

posted on 2013-07-20 22:22  Armin  阅读(472)  评论(1编辑  收藏  举报