1. 程式人生 > 資訊 >IT之家,春季招聘開始啦!

IT之家,春季招聘開始啦!

Union-Find

概述

定義:

並查集是一種樹形的資料結構,顧名思義,並和查,就是用於處理一些不相交的合併(Union)及查詢(Find)問題。常常用森林表示(見百度百科)。

操作:

並查集的操作主要有兩種:

  • 合併(Union):把兩個不相交的集合合併為一個集合。
  • 查詢(Find):確定元素屬於哪一個子集。還可以查詢兩個元素是否在同一個集合中。

重要思想:

並查集的一個最重要的思想就是:用集合中的一個元素來代表集合,既然是演算法,便是為了讓程式碼變得簡潔又高效,就像是一個團隊必定要有一個leader,不然群龍無首,效率只會更加低下。

關於並查集的思想以及概念,這篇文章將其類比為幫派和幫主,講的很好:傳送門

這裡我們通過Java語言的實現來順帶講解原理。

實現

初始化

public class UnionFindTest {
	Node[] node;   
	//並查集中的結點
	private static class Node{
		int parent;//指向父節點的指標
		boolean root;//是否為根節點,即是否為leader
		
		private Node(){
			parent = 1;
			root = true;//定義leader
		}
	}
	//將每個元素初始化為一顆單結點樹
	public UnionFindTest
(int n){ node = new Node[n + 1]; for(int i= 0; i <= n; i++){ node[i] = new Node(); } } }

find查詢

    public int find(int e){//查詢e的leader
        while(!node[e].root){ //如果e的上級不是root
            e = node[e].parent;//e繼續找父節點,直到找到leader
        }
        return e;//找到了leader
    }

    // 查詢過程, 查詢元素p所對應的集合編號
// O(h)複雜度, h為樹的高度 private int find(int p){ // 不斷去查詢自己的父親節點, 直到到達根節點 // 根節點的特點: parent[p] == p while(p != parent[p]) p = parent[p]; return p; }

Union合併

    public void union(int a,int b){
        node[a].parent += node[b].parent;//讓b的上級為a
        node[b].root = false;//令a的leader為總leader
        node[b].parent = a;
    }

路徑壓縮

優化時機是在執行 find操作 的時候對其進行路徑壓縮。

private int find(int p) {
    while (p != parents[p]) {
        parents[p] = parents[parents[p]];
        p = parents[p];
    }
    return p;
}

壓縮流程:

對比一下原來的程式碼,我們可以發現只是進行了不大的改變。

在集合的根被遞迴地找到以後,p的父結點就引用它,這對通向根的路徑上的每一個節點遞迴地出現,因此實現了路徑壓縮。

我們讓節點數少的樹往節點數多的樹合併,並使用加權標記法。

路徑壓縮完整程式碼:

建立一個介面:

public interface UF {

    int getSize();
    boolean isConnected(int p, int q);
    void unionElements(int p, int q);
}

實現類:

public class UnionFind implements UF{

    private int[] rank;   // rank[i]表示以i為根的集合所表示的樹的層數
    private int[] parent; // parent[i]表示第i個元素所指向的父節點

    // 建構函式
    public UnionFind(int size){

        rank = new int[size];
        parent = new int[size];

        // 初始化, 每一個parent[i]指向自己, 表示每一個元素自己自成一個集合
        for( int i = 0 ; i < size ; i ++ ){
            parent[i] = i;
            rank[i] = 1;
        }
    }

    @Override
    public int getSize(){
        return parent.length;
    }

    // 查詢過程, 查詢元素p所對應的集合編號
    // O(h)複雜度, h為樹的高度
    private int find(int p){
        if(p < 0 || p >= parent.length)
            throw new IllegalArgumentException("p is out of bound.");

        // 不斷去查詢自己的父親節點, 直到到達根節點
        // 根節點的特點: parent[p] == p
           while (p != parents[p]) {
            parents[p] = parents[parents[p]];
            p = parents[p];
            }
           return p;
    }

    // 檢視元素p和元素q是否所屬一個集合
    // O(h)複雜度, h為樹的高度
    @Override
    public boolean isConnected( int p , int q ){
        return find(p) == find(q);
    }

    // 合併元素p和元素q所屬的集合
    // O(h)複雜度, h為樹的高度
    @Override
    public void unionElements(int p, int q){

        int pRoot = find(p);
        int qRoot = find(q);

        if( pRoot == qRoot )
            return;

        // 根據兩個元素所在樹的rank不同判斷合併方向
        // 將rank低的集合合併到rank高的集合上
        if(rank[pRoot] < rank[qRoot])
            parent[pRoot] = qRoot;
        else if(rank[qRoot] < rank[pRoot])
            parent[qRoot] = pRoot;
        else{ // rank[pRoot] == rank[qRoot]
            parent[pRoot] = qRoot;
            rank[qRoot] += 1;   // 此時, 我維護rank的值
        }
    }
}

總結

  • 用集合中的某個元素來代表這個集合,則該元素稱為此集合的代表元;
  • 一個集合內的所有元素組織成以代表元為根的樹形結構;
  • 對於每一個元素 p,parents[p] 存放 p 在樹形結構中的父親節點(如果 p 是根節點,則令parents[p] = p);
  • 對於查詢操作,假設需要確定 p 所在的的集合,也就是確定集合的代表元。可以沿著pre[p]不斷在樹形結構中向上移動,直到到達根節點。