并查集

并查集(Union-find Sets),它主要是处理一些不相交集合的合并问题的数据结构,用于在线等价类(online equiralence class)问题。

 

  在线等价类问题中,初始时有n 个元素,每个元素都属于一个独立的等价类。需要执行以下的操作:

    1) Union(x, y):把元素 x 和元素 y 所在的集合合并。

    2) Find(x):找到元素 x 所在的集合的代表了。

 

并查集中把每个集合描述为一棵,开始时每个元素在仅包含它自己的一个集合中,如图:

若两个元素祖先相同,则它们位于同一集合。图a)和图b)中就只有一个集合,而图c)中有两个集合{15},{26,32}。

 

那么我可以很容易写出简单的并查集操作:

 1 class Simple_UnionFindSet   {
 2     public:
 3         void MakeSet(const int &N)  {
 4             for (int i = 0; i <= N; ++i)
 5                 Parent[i] = i;
 6         }
 7         int Find(int Root)  {
 8             while(Root != Parent[Root]) Root = Parent[Root];
 9             return Root;
10         }
11         void Union(const int &RootA, const int &RootB)  {
12             Parent[RootA] = RootB;
13         }
14     private:
15         int Parent[MaxN];
16 };

这时Find函数的最坏时间为O(N),M次操作的最坏时间为O(MN)在输入数据是有序的情况下,构造的BST会退化成一个链表。

 

因此我们要对性能进行优化:

  • 合并:1)重量规则:若树x 节点数少于树y 节点数,则将y 作为x 的父节点。否则将x 作为y 的父节点。

        新的构建函数:

1 void MakeSet() {
2     memset(Parent, -1, sizeof(Parent));
3     //Parent[e]的值若为正数,则表示e的父亲;
4     //Parent[e]的值若为负数,则表示e为根,且其负数为它的重量
5 }

        合并操作:

1 void Union(const int &RootA, const int &RootB)  {
2     if (Parent[RootA] < Parent[RootB]) {
3         Parent[RootA] += Parent[RootB];
4         Parent[RootB] = RootA;
5     } else {
6         Parent[RootB] += Parent[RootA];
7         Parent[RootA] = RootB;
8     }
9 }

      2)高度规则:若树x 的高度小于树y 的高度,则将y 作为x 的父节点,否则将x 作为y 的父节点。

        我们先定义一个描述高度规则的结构:

1 struct HeightNode {
2     int Parent, Height;
3     WeightNode(int _Height = 1)  {
4         Height = _Height;
5         //初始每个集合的高度都是1
6     }
7 } Node[MaxN];

        合并操作:

1 void Union(const int &RootA, const int &RootB) {
2     if (Node[RootA].Height > Node[RootB].Height)    Node[RootB].Parent = RootA;
3     else {
4         Node[RootA].Parent = RootB;
5         if(Node[RootA].Height == Node[RootB].Height)    ++Node[RootB].Height;
6     }
7 }

    →使用这两个规则都可以使树的高度 ≤ (log2N) + 1。推荐使用重量规则,因为查找函数优化后会改变树的高度,且使用重量规则可以节省空间。

 

  • 查找:1)紧缩路径(path compaction):改变从节点e到根的路径上所有节点的指针,使这些指针直接指向根节点。

 1 //递归
 2 int Find(int Root) {
 3     if(Root != Parent[Root]) Parent[Root] = Find(Parent[Root]);
 4     return Parent[Root];
 5 }
 6 
 7 //迭代
 8 int Find(int Root) {
 9     int Now, Ancestor = Root;
10     while(Parent[Ancestor] != Ancestor)    Ancestor = Parent[Ancestor];
11     while(Root != Ancestor) {
12         Now = Parent[Root];
13         Parent[Root] = Ancestor;
14         Root = Now;
15     }
16     return Root;
17 }

      2)路径分割(path splitting):改变从e 到根节点路径上每个节点(除了根和其子节点)的指针,使其指向各自的祖父节点。

1 int Find(int Root)  {
2     int Now;
3     while(Root != Parent[Root]) {
4         Now = Parent[Root];
5         Parent[Root] = Parent[Parent[Root]];
6         Root = Now;
7     }
8     return Root;
9 }

    3)路径对折(path halving):改变从e 到根节点路径上每隔一个节点(除了根和其子节点)的指针,使其指向各自的祖父节点。

1 int Find(int Root)  {
2     while(Root != Parent[Root]) {
3         Parent[Root] = Parent[Parent[Root]];
4         Root = Parent[Root];
5     }
6     return Root;
7 }

      →这三种方法中推荐使用路径对折。在实际使用中,只优化Find函数就可以达到很高的效率。

附:使用重量规则后的查找函数:

1 int Find(int Root)    {
2     while(Parent[Root] >= 0 && Parent[Parent[Root]] >= 0)
3         Root = (Parent[Root] = Parent[Parent[Root]]);
4     return Parent[Root] >= 0 ? Parent[Root] : Root;
5 }

 

 

复杂度(使用了合并和查找优化):

  设T(ƒ, µ)是交错的ƒ次查找和µ次合并操作所需的最大时间。假设µ ≥ N / 2,则 k1(N + ƒα(ƒ + N, N)) ≤ T(ƒ, µ) ≤ k2(N + ƒα(ƒ + N, N))

  • 其中αAckermann函数的反函数,因为Ackermann函数的增长很快,所以其反函数α的增长是非常慢的,可以认为α ≤ 4。

 

posted @ 2016-07-25 21:35  -搁浅-  阅读(394)  评论(0编辑  收藏  举报