1. 程式人生 > >【Algorithms公開課學習筆記1】 Union-Find 合併查詢

【Algorithms公開課學習筆記1】 Union-Find 合併查詢

Union-Find 合併查詢

動態連線性

判斷連線性的關鍵

  • 等價關係模型:如果有(a,b),(b,c),那麼也會有(a,c)。其中()表示有連線。

  • 連通分量:最大的可連通物件集合,有兩個特點:1)連通分量內部任意兩個物件都是相連通的;2)連通分量內部的物件不與外部物件相連通。

利用連通分量,可以方便地實現並查集的兩個操作:查詢請求合併命令

  • 查詢:檢查兩個物件是否在相同的連通分量中

  • 合併:將兩個物件的分享替換成其並集

快速查詢

快速查詢是基於貪心策略的一種演算法,貪心策略是指在問題求解的時候,只找出當前最優解。

基於此,設計一種資料結構來儲存實驗物件。

  • 長度為N的整型陣列

  • 如果p和q有相同的id,則表示他們有連線

因此,查詢和合並操作就變為:

  • 查詢:檢查p和q是否具有相同的id,若相同則代表連通。如圖id[1]=id[2],表示1和2相連通

  • 合併:在合併p和q物件時,將值為所有等於id[p]的值重新賦值為id[q]。如圖合併0和1,需要將id[0]=id[5]=id[6]=0的值全部賦值為id[1]=1的值

具體演算法

public class QuickFind {

    private int[] id;

    /**
     * 建構函式,初始化資料結構的屬性
     * 
     * @param N
     *            陣列額長度
     */
public QuickFind(int N) { // 初始化資料結構的值 id = new int[N]; for (int i = 0; i < N; i++) { id[i] = i; } } /** * 查詢操作,判斷連通性 * * @param p * @param q * @return */ public boolean connected(int p, int q) { return
id[p] == id[q]; } /** * 合併操作 * * @param p * @param q */ public void union(int p, int q) { int pid = id[p]; int qid = id[q]; for (int i = 0; i < id.length; i++) { if (pid == id[i]) { id[i] = qid; } } } /* setter和getter */ public int[] getId() { return id; } public void setId(int[] id) { this.id = id; } }

時間複雜度分析

演算法 初始化 合併(包含查詢) 查詢
快速查詢 N N 1

快速合併

快速合併是基於懶策略的一種演算法,懶策略是指在問題求解時儘量避免計算,直到不得不進行計算。

基於此,設計另外一種該資料結構來儲存實驗物件。

  • 長度為N的整型陣列

  • id[i]是i的父親,從而構造成樹的結構

  • 如果i=id[i],則表示id[i]是樹根

因此,合併和查詢操作就變為:

  • 查詢:檢查p和q是否具有相同的根,如有則代表連通。

  • 合併:在合併p和q物件時,將p的根的id(父親)設成q的根。

具體演算法

public class QuickUnion {

    public class QU {

        private int[] id;

        public QU(int N) {
            // 初始化屬性值
            id = new int[N];
            for (int i = 0; i < N; i++) {
                id[i] = i;
            }
        }

        /**
         * 查詢指定值的根
         * 
         * @param i
         *            待查詢根的值
         * @return
         */
        public int root(int i) {
            // 當id[i]=i時就是根
            while (id[i] != i)
                i = id[i];
            return i;
        }

        /**
         * 查詢連通性
         * 
         * @param p
         * @param q
         * @return
         */
        public boolean connected(int p, int q) {
            return root(p) == root(q);
        }

        /**
         * 合併操作,將p的根的id(父親)設成q的根
         * 
         * @param p
         * @param q
         */
        public void union(int p, int q) {
            int proot = root(p);
            int qroot = root(q);

            id[proot] = qroot;
        }

        /** getter和setter */
        public int[] getId() {
            return id;
        }

        public void setId(int[] id) {
            this.id = id;
        }
    }

時間複雜度分析

  • 查詢:取決於物件p和q的深度(而樹的深度有可能為N,線性級別)

  • 合併:常數級別,包含查詢時就是查詢的時間複雜度

演算法 初始化 合併(包含查詢) 查詢
快速查詢 N N 1
快速合併 N N N

帶權的快速合併

上面所描述的快速合併中,存在兩個缺點是:一是查詢時間消耗過大,二是樹的深度容易過大

針對上述缺點,引入帶權的快速合併演算法。該演算法的特點:

  • 改進快速合併,避免生成過高的樹

  • 追蹤每個樹的大小(物件的個數)

  • 在合併的時候,通過“將小樹連線到大樹的根”來達到平衡樹高的效果

資料結構與“快速合併”相同,合併和查詢操作為:

  • 查詢:檢查p和q是否具有相同的根,如有則代表連通(與快速合併相同)。

  • 合併:在合併p和q物件時,將小樹的根的id設為大樹的根(小樹接入大樹)。故需要額外維護資料size array來儲存樹的大小。

程式碼:修改union部分

/**
         * 合併操作,將小樹的根的id(父親)設成大樹的根
         * 
         * @param p
         * @param q
         */
        public void union(int p, int q) {
            int proot = root(p);
            int qroot = root(q);
            if(size[p]>size[q]){
                id[qroot] = proot;
                size[proot]+=size[qroot];
            }else{
                id[proot] = qroot;
                size[qroot]+=size[proot];
            }
        }

時間複雜度分析

  • 查詢:取決於物件p和q的深度(而樹的深度最多為lgN)

  • 合併:常數級別,包含查詢時就是查詢的時間複雜度

演算法 初始化 合併(包含查詢) 查詢
快速查詢 N N 1
快速合併 N N N
帶權快速合併 N lgN lgN

帶壓縮路徑的快速合併

在做合併和查詢找之前,將樹的路徑進行壓縮,從而保持樹的扁平化。

程式碼實現

    public int root(int i) {
            // 當id[i]=i時就是根
            while (id[i] != i){
                //將i的根指向其爺爺輩
                id[i]=id[id[i]];
                i = id[i];
            }
            return i;
        }

時間對複雜度比

對於在N個物件中有M個並查集的情況

演算法 時間
快速查詢 MN
快速合併 MN
帶權快速合併 N+MlogN
帶壓縮路徑快速合併 N+MlogN

第1周作業的答案