JAVA拾遺 - 並查集算法的實現與改進

並查集

並查集所需要實現的主要有一下幾個功能
1.建立新的集合
2.查找某個元素屬於哪個集合
3.合併兩個集合

我們希望它的算法複雜度達到O(1),那麼具體應該如何實現呢?

方法

UF(int n) //建立並查集
int count() //返回並查集集合數
boolean connected(int a, int b) //測試兩個元素是否在同一個集合內
find(int a) //返回某個元素屬於哪個集合
union(int a, int b)//對兩個點或者這兩個點所屬的元素完成並操作

思路

並查集是一類樹狀結構,非雙向,所以在實現union中應該先實現find(),因爲並的兩個元素可能因爲從屬關係已經相併,或者二者是另外一個大集合的元素。

在實現查操作(find)時,我們維護一個id[]數組用以存儲每個點的label屬性,使用find時將對返回對應點的對應id,同時對於實現並操作時必須滿足維護id[]數組這個條件

成本:在實現unionfind算法時,其成本可視爲訪問id[]數組的次數。

1. Quick-Find

代碼如下:

public int find(int p){  //find的速度會非常的快
    return id[p];
}
public void union(int p, int q){ //union需要進行遍歷,相對來說會比較慢
    if(find(p) == find(q))
        return;
    for(int i = 0 ; i < id.length ; i++){
        if(id[i] == find(p)) //這裏應該先將p的id計算出來效率會比較快
            id[i] = find(q);
    }
    count--;
}

**分析:**find的速度會非常的快,但是Quick-Find算法無法面對較大的集合,因爲每一次使用Union時都需要對原數組進行一次掃描,其複雜度至少是O(N**2)——>(N+3的單次調用乘上N-1次的最壞情況下調用)

所以我們提出Quick-Union

2.Quick-Union

代碼如下

private int find(int p){
    while(p != id[p]){
        p = id[p];             //查找根節點
    }
    return p;
}

void Union(int p , int q){
    int pRoot = find(p);
    int qRoot = find(q);
    if(pRoot == qRoot){
        return;
    }
    id[pRoot] = qRoot;
    count --;
}

分析:在這種QuickUnion的情況下,存儲形式的樹形結構被強調了,每個點的id存放的是它父節點的位置,這樣的Union方法會快上很多,同時Connected()方法也需要遍歷到根節點再進行對比。
同時Find方法的時間複雜度爲N,M對Find的複雜度爲o(N*M)—->find總是會遍歷一棵樹,可能這棵樹太大太大

所以我們提出加權的Quick-Union方法,在這個算法中我們總是把小樹連接到大樹上

3.加權Quick-Union

爲了實現加權的QU,我們需要定義幾個新的變量,代碼如下

private int[] size;
private int[] id;
WeightQuickUnionUF(int N){
    count = N;
    id = new int[N];
    size = new int[N];
    for(int i = 0 ; i <N ; i ++){
        id[i] = i;
    }
    for(int i = 0 ; i < N ; i++){
        size[i] = 1;
    }
}
public int find( int p){
    while(p != id[p]) p = id[p];
    return p;
}
public void union(int p , int q){
    int i = find(p);
    int j = find(q);
    if(i == j) return;
    if(size[i] < size[j]){
    id[i] = i;
    size[j] += size[i];
    }
    else{
    id[j] = i;
    size[i] += size[j];
    }
    count --;
}

複雜度MlgN

爲了實現O(1)的均攤複雜度,可以在每次Union時對路徑進行壓縮,儘量縮短樹的度,這裏提供這個思路,各位水友自行解決吧~

最後轉載個特別好玩的並查集思路

爲了解釋並查集的原理,我將舉一個更有愛的例子。 話說江湖上散落着各式各樣的大俠,有上千個之多。他們沒有什麼正當職業,整天揹着劍在外面走來走去,碰到和自己不是一路人的,就免不了要打一架。但大俠們有一個優點就是講義氣,絕對不打自己的朋友。而且他們信奉“朋友的朋友就是我的朋友”,只要是能通過朋友關係串聯起來的,不管拐了多少個彎,都認爲是自己人。這樣一來,江湖上就形成了一個一個的羣落,通過兩兩之間的朋友關係串聯起來。而不在同一個羣落的人,無論如何都無法通過朋友關係連起來,於是就可以放心往死了打。但是兩個原本互不相識的人,如何判斷是否屬於一個朋友圈呢?我們可以在每個朋友圈內推舉出一個比較有名望的人,作爲該圈子的代表人物,這樣,每個圈子就可以這樣命名“齊達內朋友之隊”“羅納爾多朋友之隊”……兩人只要互相對一下自己的隊長是不是同一個人,就可以確定敵友關係了。但是還有問題啊,大俠們只知道自己直接的朋友是誰,很多人壓根就不認識隊長,要判斷自己的隊長是誰,只能漫無目的的通過朋友的朋友關係問下去:“你是不是隊長?你是不是隊長?”這樣一來,隊長面子上掛不住了,而且效率太低,還有可能陷入無限循環中。於是隊長下令,重新組隊。隊內所有人實行分等級制度,形成樹狀結構,我隊長就是根節點,下面分別是二級隊員、三級隊員。每個人只要記住自己的上級是誰就行了。遇到判斷敵友的時候,只要一層層向上問,直到最高層,就可以在短時間內確定隊長是誰了。由於我們關心的只是兩個人之間是否連通,至於他們是如何連通的,以及每個圈子內部的結構是怎樣的,甚至隊長是誰,並不重要。所以我們可以放任隊長隨意重新組隊,只要不搞錯敵友關係就好了。於是,門派產生了。
                    來自http://www.cnblogs.com/TonyNeal/p/bingchaji.html

UPDATE!最近研究了下路徑壓縮,然後寫了下面的代碼

package UF;

import edu.princeton.cs.introcs.StdOut;

public class UF_sample_3 {
    /**
     * now we try to make the Quick-Union tree more short
     * 
     * Modify quick-union to avoid tall trees
     * 
     * Keep track of size of each tree(number of object)
     * 
     * Balance by ** linking root of smaller tree to roof of large trss. **
     * 
     * WEIGHT UNION
     * 
     * Initialized N
     * 
     * Union lgN ↑
     * 
     * Connected lgN
     */
    public int[] sz ; //size tree to store the number of son that every roots have 
    public int[] id ;
    public int count ;
    public UF_sample_3(int n){
        id = new int[n];
        sz = new int[n];
        count = n ;
        for(int i = 0 ; i < n ; i++){
            id[i] = i ;
            sz[i] = 1 ;
        }
    }
    public int getRoot(int num){
        while(num!=id[num]){
            id[num] = id[id[num]]; //這裏是關鍵哦!
            num = id[num];
        }
        return num ;
    }
    public void union(int num1 , int num2){
        int pid = getRoot(num1) ; 
        int qid = getRoot(num2) ;
        if(sz[pid] < sz[qid]){id[pid] = qid ; sz[qid] = sz[qid] + sz[pid];}
        else{id[qid] = pid ; sz[pid] = sz[qid] + sz[pid];}
    }
    public boolean connected(int num1 , int num2){
        if(getRoot(num1) == getRoot(num2)){
            StdOut.println(num1 + ","+ num2 + " they are connected");
            return true;
        }

        else {
            StdOut.println(num1 + ","+ num2 + " they are not connected");
            return false;
        }
    }
    public void println(){
        StringBuilder str = new StringBuilder();
        for(int i = 0 ; i < count ; i++){
            str.append(id[i] +",");
        }
        StdOut.println(str.toString());
    }




    public static void main(String[] args) {
        // TODO Auto-generated method stub
        UF_sample_3 uf = new UF_sample_3(10);
        uf.union(4, 3);
        uf.println();
        uf.union(3, 8);
        uf.println();
        uf.union(6, 5);
        uf.println();
        uf.union(9, 4);
        uf.println();
        uf.union(2, 1);
        uf.println();
        uf.union(8, 9);
        uf.println();
        uf.connected(5, 0);
        uf.union(5, 0);
        uf.connected(5, 0);
        uf.println();
        uf.union(7, 2);
        uf.println();
        uf.union(6, 1);
        uf.println();
        uf.union(7, 3);
        uf.println();
    }
}

大家自己試試吧,關鍵的那段代碼我已經標註了~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章