1. 程式人生 > >普林斯頓大學_演算法公開課:Part_1_第一部分:並查集

普林斯頓大學_演算法公開課:Part_1_第一部分:並查集

首先,給大家推薦一個平臺,Coursera (類比國內的mooc網),你可以在上面學習諸多國外一流大學的公開課視訊,各個領域的都有,涉獵範圍很廣。想要出國留學的小夥伴兒不妨在上面事先感受一波國外授課的氛圍與模式。

言歸正傳,作為一名程式猿,演算法實在是太重要了!即便不是一名程式設計師,學習一下演算法程式設計也是很有好處的。因為,在某種層面上講,演算法是你針對計算機進行的解決問題的思想上的對映,程式碼就是你想法的載體,邏輯的展現形式和與計算機進行溝通的工具。隨著學習的深入,你會發現好多程式設計的技巧其實都源於生活!學習程式設計就是感悟生命!當然,在某種程度上,你也可以理解為我在胡說八道!哈哈!

下面介紹一下我學習普林斯頓大學_演算法公開課:Part_1的經歷吧!

由於是回顧課程,所以重點放在對核心演算法的理解與剖析上:

第一部分:並查集

問題描述:你可以把下面這個圖整體想象成是一個地下管道通水系統。黑色的是土,白色的是空管道,藍色的是通水的管道。

顯然,水是從上往下流的,只有靠近最上邊界的管道才可以通水。同時,這個通水的過程是有壓力的,不知道是不是有一個水泵,還是自然現象,凡是聯通的水管情況是保持一致的,即不管管道是否是逆生長,既然你們手拉手是聯通的,那就是一夥兒的,要麼同流合汙,要麼空空如也!這裡有一個關鍵點,你在程式設計中會注意到:下邊界不是自聯通的。而上邊界,你可以想象成在其上方有一個水庫,凡是靠近邊界的水管都有水喝,呵呵!說了這麼多廢話,那麼問題究竟是什麼呢?就是針對一大片直立的黑土地,隨機的鋪設管道,啥時候水管才能Percolate?Percolate啥意識呢?就是上下邊界中存在至少一條聯通的水流!


答案是:

mean=                   0.59229975
stddev=                 0.011781283036553099

95% confidence interval=[0.5899906185248356,0.5946088814751644]

就是說大概隨機管道鋪設覆蓋率達到60%,上下邊界水流就通了!

怎麼樣,60分及格不無道理吧!程式設計即生活吧!

並查集核心演算法:

package unionFind;


public class SelfUN {
private int count;
private int[] parent;
//private int[] size;//記錄以當前節點為根節點的子樹擁有的節點總數。


public SelfUN(int n) {//節點初始化。
count = n;
parent = new int[n];
//size = new int[n];
for (int i = 0; i < n; i++) {//初始狀態每個節點的父節點是其自身。
parent[i] = i;
//size[i] = 1;
}
}


public int getCount() {//獲取並查集中節點總數。
return count;
}


private void validate(int p) {//節點是否有效判別。
try {
if (p < 0 || p >= count) {
throw new IllegalArgumentException("");
}


} catch (Exception e) {
System.out.println("The parameter is wrong!");
}


}


public int find(int p) {//查集
validate(p);
int root = p;
while (parent[root] != root) {//找到根節點
root = parent[root];
}
while (p != root) {//路徑壓縮,將除根節點以外的所有節點都直指根節點。即每一次find都是一次優化。
int tmp = parent[p];
parent[p] = root;
p = tmp;
}
return root;
}


public boolean connected(int p, int q) {//追根溯源,檢視兩個節點是否具有相同的根節點,進而判別是否互相聯通。
return find(p) == find(q);
}


public void union(int p, int q) {//將兩個節點並集,歸為一類。
if (find(p) == find(q))//如果原本互聯,則無需操作。
return;
/* 此處保證總是將較小的那棵樹合併到較大的那棵樹上,以原較大的樹的根節點作為合併後的根節點的好處是使得樹更均衡,也是一種路徑上的優化。
* 一種極端的情況是:路徑壓縮過程中可以保證遍歷的是節點更少的那棵樹,時間複雜度在一定程度上降低了。
* if(size[p]<=size[q]) { parent[p]=q; size[q]+=size[p]; }else { parent[q]=p;
* size[p]+=size[q]; }
*/
// count--;
if (p <= q) {//此處類比大根堆,遵循某種規律實際上也是一種路徑上的優化,與size的引入同理。
parent[p] = q;
} else {
parent[q] = p;
}
}


}

演算法評論:

尼古拉斯·沃斯說過:演算法+資料結構=程式

本人認為:演算法是一種抽象的邏輯思維,而資料結構作為實現演算法的描述基礎,對於演算法的衍生和實現起到了舉足輕重的作用。合理的資料結構是優質演算法的第一步,一定程度上左右著演算法的走向,很是關鍵。

並查集的資料結構就是樹。簡單的陣列卻巧妙的描述了父子節點間的關係:陣列下標為子節點,陣列值為父節點。當父子節點不斷聯通,關係也變得複雜起來,比如某個節點是另一個節點七大姑八大姨的兒子的舅舅的姥爺的姥姥的侄子,你會想?What?!!但如果告訴你,追根溯源你們有相同的老祖宗,500年前是一家啊!是不是關係就清晰明瞭了。管他三七二十一,反正500年前是一家,都是親戚。並查集所關注的正式這個源頭。而樹所包含的邏輯關係很好的描述了追根溯源這一流程。樹的根節點就是源頭,就是傳說中的老祖宗!

演算法應用:

應用一,先給大家熱個身,玩個裁剪小遊戲。問題不作描述,自己理解吧!

package unionFind;


import java.util.Scanner;


public class Successor {
private SelfUN un;
private boolean[] num;


public Successor(int n) {
un = new SelfUN(n);
num = new boolean[n];
for (int i = 0; i < n; i++) {
num[i] = false;//預設沒有被裁剪掉。
}
}


public int getValue(int p) {
if(num[p]==false)return p;//沒有被裁剪掉,則返回其本身。
//if(p==un.getCount())return p;
int root = un.find(p);
if (root == un.getCount())//陣列的範圍,也就是節點的範圍是:0到n-1.這裡count的值為n。
return -1;//最後一個節點被裁剪後,其返回的值為-1.
return un.find(p) + 1;//其餘節點被裁剪後,其返回的值是當前節點鏈的最後一個節點的下一個節點。
}


public void remove(int p) {
if (num[p] == true)
return;//已被裁剪無需操作。
num[p] = true;//標註已被裁剪。
if (p!=0&&!un.connected(p - 1, p)&&num[p-1]==true) {//判別非第一節點的前一個節點是否亦被裁剪。
un.union(p - 1, p);//是的話,合併。
}
if ((p+1)!=un.getCount()&&!un.connected(p, p + 1)&&num[p+1]==true) {//判別非最後一個節點的後一個節點是否亦被裁剪掉。
un.union(p, p + 1);//是的話,合併。注意,這裡並查集預設是將後一個節點作為根節點合併的。
}
}


public static void main(String args[]) {
Scanner sc = new Scanner(System.in);
System.out.println("Please input the parameter:");
int num = sc.nextInt();
Successor s = new Successor(num);
s.remove(1);
System.out.println(s.getValue(1));
System.out.println(s.getValue(0));
s.remove(2);
System.out.println(s.getValue(1));
s.remove(3);
System.out.println(s.getValue(1));
s.remove(4);
System.out.println(s.getValue(1));
sc.close();


}

}

應用二,Percolation,對的,就是上面提到過的,60分萬歲,管道達到60%覆蓋率上下邊界流通。

使用並查集去追根溯源還是挺簡單的,只需判斷上下邊界是否聯通就ok啦!比如一個n*n+1的並查集,0和n*n+1分別代表上下邊界,但在實現過程中存在一個問題:backwash,即迴流。迴流是啥呢?就是與底邊界聯通的,同時與上邊界非聯通的管道可能被誤判為有水流注入,原因是並查集過程中的追根溯源過程只有一個老祖宗!!!我們將那些只與下邊界聯通的管道同那些與上下邊界同時聯通的管道一視同仁,追根同源。而實際情況是下邊界是黑土地,上邊界是大水池。僅僅同時聯通黑土地的管道並沒有任何關係。

想法一,使用兩個並查集,一個判別是否同時與上下兩個邊界同時聯通,另一個判別是否與上邊界聯通,而非迴流照成的誤判。

程式碼如下:

package unionFind;


import edu.princeton.cs.algs4.WeightedQuickUnionUF;
import edu.princeton.cs.algs4.StdOut;
//import edu.princeton.cs.algs4.StdIn;


public class Percolation1 {
private int count;
private WeightedQuickUnionUF un;//並查集,用於判別是否與上下邊界同時聯通。
private WeightedQuickUnionUF unTop;//並查集,僅用於判別是否與上邊界聯通。
private boolean[] tag;//標記,判別是否有鋪設管道。
// private boolean isPercolates=false;
// private int row, col;


public Percolation1(int n) {//建構函式初始化。
if (n < 0)
throw new IllegalArgumentException("Illegal Argument!");
count = n;
un = new WeightedQuickUnionUF(n * n + 2);
unTop = new WeightedQuickUnionUF(n * n + 1);
tag = new boolean[n * n + 2];
tag[0] = true;
tag[n * n + 1] = true;
for (int i = 1; i < n * n + 1; i++) {
tag[i] = false;
}
tag[n * n + 1] = true;//用於上下邊界聯通判別,類比tag[0] = true.
}


public void validate(int p) {
if (p < 1 || p > count)
throw new IllegalArgumentException("Illegal Argument!");
}


    public void open(int row, int col) {
        validate(row);
        validate(col);
        int curIndex = (row - 1) * count + col;
        if (tag[curIndex])
            return;
        tag[curIndex] = true;
        if (row == 1) {
            un.union(0, curIndex);
            unTop.union(0, curIndex);//該並查集僅用於判別當前管道與上邊界是否聯通。
        }
        if (row == count) {//下邊界與上邊界此處處於等價關係、下邊界類比上邊界倒置情形。
            un.union(count * count + 1, curIndex);
        }
        //當前鋪設的管道與其四周的管道進行連線,放入同一個集合。
        if (col - 1 >= 1 && tag[curIndex - 1]) {
            un.union(curIndex, curIndex - 1);
            unTop.union(curIndex, curIndex - 1);
        }
        if (col + 1 <= count && tag[curIndex + 1]) {
            un.union(curIndex, curIndex + 1);
            unTop.union(curIndex, curIndex + 1);
        }
        if (row - 1 >= 1 && tag[curIndex - count]) {
            un.union(curIndex, curIndex - count);
            unTop.union(curIndex, curIndex - count);
        }
        if (row + 1 <= count && tag[curIndex + count]) {
            un.union(curIndex, curIndex + count);
            unTop.union(curIndex, curIndex + count);
        }


    }


public boolean isOpen(int row, int col) {
int curIndex = (row - 1) * count + col;
return tag[curIndex];//是否有管道鋪設的標記位。
}


public boolean isFull(int row, int col) {
int curIndex = (row - 1) * count + col;
if (un.connected(0, curIndex) && un.connected(count * count + 1, curIndex) && unTop.connected(0, curIndex)) {
return true;//是否為通水管道判別,unTop用於防止迴流。
} else {
return false;
}
}


public int numberOfOpenSites() {
int calcu = 0;
for (int i = 1; i <= count * count; i++) {
if (tag[i] == true)
calcu++;//總的管道數目。
}
return calcu;
}


public boolean percolates() {
for (int i = 1; i <= count; i++) {
for (int j = 1; j <= count; j++) {
if (isOpen(i, j) && isFull(i, j)) {
return true;//遍歷每一塊黑土地,尋找通水管道,如果存在,則percolate.
}
}
}
return false;
}


public static void main(String[] args) {
Percolation1 p = new Percolation1(1);
p.open(1, 1);
//p.isOpen(1, 1);
//p.open(3, 7);
//StdOut.println(p.isOpen(1, 1));
        //StdOut.println(p.isOpen(1, 2));
//StdOut.println(p.isFull(1, 1));
//p.open(2, 1);
StdOut.println(p.isFull(1, 1));
StdOut.println(p.percolates());


}

}

上述程式碼在percolates函式與isFull函式的編寫上不夠精簡,下面對其進行修改,這裡不作解釋,請自行體會!

package unionFind;
import edu.princeton.cs.algs4.WeightedQuickUnionUF;
import edu.princeton.cs.algs4.StdOut;
public class Percolation2 {
    private int curOpen = 0;
    private final int count;
    private final WeightedQuickUnionUF unTop;
    private final WeightedQuickUnionUF un;
    private boolean[] tag;
    public Percolation2(int n) {
        if (n < 1) {
            throw new IllegalArgumentException("Illegal Argument!");
        }
        count = n;
        unTop = new WeightedQuickUnionUF(n * n + 1);
        un = new WeightedQuickUnionUF(n * n + 2);
        tag = new boolean[n * n + 2];
        tag[0] = true;
        tag[n * n + 1] = true;
        for (int i = 1; i < n * n + 1; i++) {
            tag[i] = false;
        }
        tag[n * n + 1] = true;
    }
    private void validate(int p) {
        if (p < 1 || p > count) {
            throw new IllegalArgumentException("Illegal Argument!");
        }
    }
    public void open(int row, int col) {
        validate(row);
        validate(col);
        int curIndex = (row - 1) * count + col;
        if (tag[curIndex])
            return;
        curOpen++;
        tag[curIndex] = true;
        if (row == 1) {
            unTop.union(0, curIndex);
            un.union(0, curIndex);
        }
        if (row == count) {
            un.union(count * count + 1, curIndex);
        }
        if (col - 1 >= 1 && tag[curIndex - 1]) {
            unTop.union(curIndex, curIndex - 1);
            un.union(curIndex, curIndex - 1);
        }
        if (col + 1 <= count && tag[curIndex + 1]) {
            unTop.union(curIndex, curIndex + 1);
            un.union(curIndex, curIndex + 1);
        }
        if (row - 1 >= 1 && tag[curIndex - count]) {
            unTop.union(curIndex, curIndex - count);
            un.union(curIndex, curIndex - count);
        }
        if (row + 1 <= count && tag[curIndex + count]) {
            unTop.union(curIndex, curIndex + count);
            un.union(curIndex, curIndex + count);
        }
    }
    public boolean isOpen(int row, int col) {
        validate(row);
        validate(col);
        int curIndex = (row - 1) * count + col;
        return tag[curIndex];
    }
    public boolean isFull(int row, int col) {
        validate(row);
        validate(col);
        int curIndex = (row - 1) * count + col;
        if (unTop.connected(0, curIndex)) {
            return true;
        }
        return false;
    }
    public int numberOfOpenSites() {
        return curOpen;
    }
    public boolean percolates() {
        if (numberOfOpenSites() < count) {
            return false;
        }
        if (un.connected(0, count * count + 1)) {
            return true;
        }
        return false;
    }


    public static void main(String[] args) {
        Percolation2 p = new Percolation2(1);
        p.open(1, 1);
        StdOut.println(p.isFull(1, 1));
        StdOut.println(p.percolates());
    }

}

想法二,可不可以只用一個並查集呢?進而節省空間和提升效率。答案是:當然可以!

package unionFind;

import edu.princeton.cs.algs4.WeightedQuickUnionUF;
import edu.princeton.cs.algs4.StdOut;

public class Percolation {
    private int curOpen = 0;
    private final int count;
    private final WeightedQuickUnionUF un;

    private byte[] tag;

    // 之前的boolean型別的標記位只能描述兩種狀態,即管道與黑土。而通水管道是用un.union(0, curIndex)判別的。

   // 我們應該注意到,之所以造成迴流的原因是我們運用了不恰當的資料結構來描述與底部邊界聯接的管道。

   // 它不是一棵樹。所以我們完全可以僅僅是增添標記去單獨標明那些與底部邊界相連的管道。

    //這裡我們將其設定為tag[curIndex]=2來表示。

    public Percolation(int n) {// n是整個黑土地的邊長,預設是一個方形。
        if (n < 1) {
            throw new IllegalArgumentException("Illegal Argument!");
        }
        count = n;
        un = new WeightedQuickUnionUF(n * n + 1);
        tag = new byte[n * n + 1];
        tag[0] = 1;// 1表示通水管道。
        for (int i = 1; i < n * n + 1; i++) {
            tag[i] = 0;// 0表示黑土地。
        }
    }
    private void validate(int p) {
        if (p < 1 || p > count) {
            throw new IllegalArgumentException("Illegal Argument!");
        }
    }
    public void open(int row, int col) {
        validate(row);
        validate(col);
        int curIndex = (row - 1) * count + col;
        if (tag[curIndex] > 0)
            return;
        curOpen++;// 記錄管道個數。
        tag[curIndex] = 1;
        if (row == 1) {
            un.union(0, curIndex);
        }
        if (count == 1) {// 僅有一塊黑土地鋪設一塊管道的情形,也是percolates的。
            tag[un.find(0)] = 2;// 僅有一塊的情形是與底部聯通的。
            return;
        }
        if (row == count) {// 最後一行鋪設的管道,與大地邊界聯通,標記為2.
            tag[curIndex] = 2;
        }
        if (col - 1 >= 1 && tag[curIndex - 1] > 0) {// 當前管道左邊有管道。
            if (tag[un.find(curIndex - 1)] == 2 || tag[un.find(curIndex)] == 2) {
                un.union(curIndex, curIndex - 1);
                tag[un.find(curIndex)] = 2;// 合併後的根節點也標註為2.
                // 這裡我們應該意識到,我們並不是將每一個與底部聯通的管道都標註為2.而是用僅僅標註其根節點來描述整棵樹上的節點都是與底部聯通的。
            } else {
                un.union(curIndex, curIndex - 1);
            }
        }
        if (col + 1 <= count && tag[curIndex + 1] > 0) {// 當前管道的右邊有管道。
            if (tag[un.find(curIndex + 1)] == 2 || tag[un.find(curIndex)] == 2) {
                un.union(curIndex, curIndex + 1);
                tag[un.find(curIndex)] = 2;
            } else {
                un.union(curIndex, curIndex + 1);
            }
        }
        if (row - 1 >= 1 && tag[curIndex - count] > 0) {// 當前管道的上邊有管道。
            if (tag[un.find(curIndex - count)] == 2 || tag[un.find(curIndex)] == 2) {
                un.union(curIndex, curIndex - count);
                tag[un.find(curIndex)] = 2;
            } else {
                un.union(curIndex, curIndex - count);
            }
        }
        if (row + 1 <= count && tag[curIndex + count] > 0) {// 當前管道的下邊有管道。
            if (tag[un.find(curIndex + count)] == 2 || tag[un.find(curIndex)] == 2) {
                un.union(curIndex, curIndex + count);
                tag[un.find(curIndex)] = 2;
            } else {
                un.union(curIndex, curIndex + count);
            }
        }
    }
    public boolean isOpen(int row, int col) {
        validate(row);
        validate(col);
        return tag[(row - 1) * count + col] > 0;// 判別是管道還是黑土。
    }
    public boolean isFull(int row, int col) {
        validate(row);
        validate(col);
        if (isOpen(row, col) && un.connected(0, (row - 1) * count + col)) {// 判別是否是通水管道。
            return true;
        }
        return false;
    }
    public int numberOfOpenSites() {
        return curOpen;// 返回管道數目。
    }
    public boolean percolates() {
        if (numberOfOpenSites() < count) {// 如果鋪設的管道小於整個黑土地的邊長,那麼percolates是不可能的。
            return false;
        }
        return tag[un.find(0)] == 2;// 與上邊界聯通且與下邊界聯通。
    }
    public static void main(String[] args) {
        Percolation p = new Percolation(2);
        p.open(1, 1);
        p.open(2, 1);
        StdOut.println(p.isFull(1, 1));
        StdOut.println(p.percolates());
    }

}

後面的課程請參見:https://blog.csdn.net/GZHarryAnonymous/article/details/80398106