并查集

资料来源于The algorithm design manual

问题引入

考虑以下2个操作:

Same component(v1,v2)– Do vertices v1 and v2 occur in the same connected

component of the current graph?(判断2个点是否在图中的同一个连通分量中)

Merge components(C1,C2) – Merge the given pair of connected components

into one component in response to an edge between them.(合并两个连通分量为一个连通分量)

component是一个连通分量,它有可能是由1个点组成的分量,但它不是1个点。

考虑2种可能的数据结构:

  1. 使用component number标记每个节点。在这种情况下,Same component()的时间复杂度是O(1),但Merge components()的复杂度将会是O(N),因为需要更新component number。
  2. 使用图,将Merge视作插入一条边。在这种情况下,Merge components()的时间复杂度是O(1),但Same component()的复杂度将会是O(N),因为可能需要遍历全图。

并查集就是为了解决上述问题而诞生的。

并查集定义

文字描述:并查集使用回溯树表示每个子集,其中节点均指向其父节点。每个节点中包含一个子节点的元素。每个子集的名称取决于对应子树的根节点。每个节点所在子树的节点数也需要被保存

代码描述:

typedef struct {
    int p[SET_SIZE+1]; /* parent element */
    int size[SET_SIZE+1]; /* number of elements in subtree i */
    int n; /* number of elements in set */
} set_union;

并查集接口定义

2个接口分别对应需要实现的2个操作

Find(i) – Find the root of tree containing element i, by walking up the parent pointers until there is nowhere to go. Return the label of the root.(沿着父指针向上查找,找到元素i所在树的根)

Merge components(C1,C2) – Merge the given pair of connected components

into one component in response to an edge between them.(通过增加一条边的方式,合并指定的2个连通分量成1个连通分量)

实现复杂度分析:

复杂度来源于树的不平衡性,因此需要限制树高。

如何限制树的高度呢?因为在union时,合并得到的新树的根节点会从2棵被合并的树的根节点中选出。因此,最直观的限制树高的办法就是令较小的树成为较大的树的子树,即选择较大的树的根节点为新树的根节点

原因: 对于这种做法,在union后,虽然较小的树中所有节点所在的高度+1,但是较大的树的高度不变。

并查集接口实现

  1. C实现:

    set_union_init(set_union *s, int n)
    {
        int i; /* counter */
        for (i=1; i<=n; i++) {
            s->p[i] = i;
            s->size[i] = 1;
        }
        s->n = n;
    }
    
    int find(set_union *s, int x)
    {
        if (s->p[x] == x)
            return(x);
        else
            return( find(s,s->p[x]) );
    }
    
    int union_sets(set_union *s, int s1, int s2)
    {
        int r1, r2; /* roots of sets */
        r1 = find(s,s1);
        r2 = find(s,s2);
        if (r1 == r2) return; /* already in same set */
        if (s->size[r1] >= s->size[r2]) {
            s->size[r1] = s->size[r1] + s->size[r2];
            s->p[ r2 ] = r1;
        }
        else {
            s->size[r2] = s->size[r1] + s->size[r2];
            s->p[ r1 ] = r2;
        } 
    }
    
    bool same_component(set_union *s, int s1, int s2)
    {
        return ( find(s,s1) == find(s,s2) );
    }
    
  2. Java实现:

    1. 最初版

         public class UnionFindSet {
         
             /**
              *  number of elements in set
              */
             int size;
         
             /**
              *  number of elements in subtree i
              */
             int[] treeSize;
         
             /**
              *  parent element
              */
             int[] parentIndex;
         
             public UnionFindSet(int size) {
                 parentIndex = new int[size+1];
                 treeSize = new int[size+1];
                 for (int i = 1; i <= size; i++) {
                     parentIndex[i] = i;
                     treeSize[i] = 1;
                 }
                 this.size = size;
             }
         
             public int find(int x) {
                 if (parentIndex[x] == x) {
                     return x;
                 } else {
                     return find(parentIndex[x]);
                 }
                 
                 //压缩路径版本
                 if(parentIndex[x] != x) {
                     parentIndex[x] = find(parentIndex[x]);
                 }
                 return parentIndex[x];
             }
         
             public boolean sameComponent(int s1, int s2) {
                 return find(s1) == find(s2);
             }
         
             public void union(int s1, int s2) {
                 int r1 = find(s1);
                 int r2 = find(s2);
         
                 if (r1 == r2) {
                     return;
                 }
         
                 if (treeSize[r1] >= treeSize[r2]) {
                     parentIndex[r2] = r1;
                     treeSize[r1] += treeSize[r2];
                 } else {
                     parentIndex[r1] = r2;
                     treeSize[r2] += treeSize[r1];
                 }
             }
         }
         
      
    2. 泛型版,压缩查找路径

      public class UnionFindSetImpl {
      
      	public static class Element<V> {
      		public V value;
      
      		public Element(V value) {
      			this.value = value;
      		}
      
      	}
      
      	public static class UnionFindSet<V> {
      		public HashMap<V, Element<V>> elementMap;
      		public HashMap<Element<V>, Element<V>> fatherMap;
      		public HashMap<Element<V>, Integer> rankMap;
      
      		public UnionFindSet(List<V> list) {
      			elementMap = new HashMap<>();
      			fatherMap = new HashMap<>();
      			rankMap = new HashMap<>();
      			for (V value : list) {
      				Element<V> element = new Element<V>(value);
      				elementMap.put(value, element);
      				fatherMap.put(element, element);
      				rankMap.put(element, 1);
      			}
      		}
       
      		private Element<V> findHead(Element<V> element) {
      			Stack<Element<V>> path = new Stack<>();
      			while (element != fatherMap.get(element)) {
      				path.push(element);
      				element = fatherMap.get(element);
      			}
                  //压缩查找路径
      			while (!path.isEmpty()) {
      				fatherMap.put(path.pop(), element);
      			}
      			return element;
                  
                  //或者这样实现
              //    if(fatherMap.get(element) != element) {
              //        fatherMap.put(element, findHead(element));
              //    }
      //            return fatherMap.get(element);
      		}
      
      		public boolean isSameSet(V a, V b) {
      			if (elementMap.containsKey(a) && elementMap.containsKey(b)) {
      				return findHead(elementMap.get(a)) == findHead(elementMap
      						.get(b));
      			}
      			return false;
      		}
      
      		public void union(V a, V b) {
      			if (elementMap.containsKey(a) && elementMap.containsKey(b)) {
      				Element<V> aF = findHead(elementMap.get(a));
      				Element<V> bF = findHead(elementMap.get(b));
      				if (aF != bF) {
      					Element<V> big = rankMap.get(aF) >= rankMap.get(bF) ? aF : bF;
      					Element<V> small = big == aF ? bF : aF;
      					fatherMap.put(small, big);
      					rankMap.put(big, rankMap.get(aF) + rankMap.get(bF));
      					rankMap.remove(small);
      				}
      			}
      		}
      
      	}
      
      }
      

复杂度分析

On each union, the tree with fewer nodes becomes the child. But how tall can such a

tree get as a function of the number of nodes in it? Consider the smallest possible

tree of height h. Single-node trees have height 1. The smallest tree of height-2

has two nodes; from the union of two single-node trees. When do we increase the

height? Merging in single-node trees won’t do it, since they just become children

of the rooted tree of height-2. Only when we merge two height-2 trees together do

we get a tree of height-3, now with four nodes.

See the pattern? We must double the number of nodes in the tree to get an

extra unit of height. How many doublings can we do before we use up all n nodes?

At most, lg2 n doublings can be performed. Thus, we can do both unions and fifinds

in O(log n), good enough for Kruskal’s algorithm.

翻译: 每次union,若树的高度增加1,则树的节点至少翻倍。因此,对于N个节点,经过多次翻倍,当树的高度达到最大时,翻倍的次数时logN,这意味着树的最大高度时logN

改进前:时间复杂度O(logN)

压缩查找路径(path compression)后,每次findHead都会将路径上的节点的深度变为1。假设1条路径上有k个节点,对于这k个节点,第一次findHead的时间复杂度为O(k),从第2次开始findHead的时间复杂度变回O(1),将第一次findHead的时间均摊到后面,平均空间和时间复杂度能够

改进后:时间平均复杂度O(1)

不准不开心。
原文地址:https://www.cnblogs.com/iltonmi/p/14327479.html