貪心算法解決單源最短路徑問題

參考教材:算法設計與分析(第3版) 王曉東 編著 清華大學出版社

貪心算法總是做出在當前看來最好的選擇,也就是說貪心算法並不從整體最優考慮,它所做出的選擇只是在某種意義上的局部最優選擇。

貪心算法的基本要素
1. 貪心選擇性質
指所求問題的整體最優解可以通過一系列局部最優的選擇,即貪心選擇來達到。
這是貪心算法可行的第一個基本要素,也是貪心算法與動態規劃算法的主要區別。
2. 最優子結構性質
當一個問題的最優解包含其子問題的最優解時,稱此問題具有最優子結構性質。
拿單源最短路徑當例子,從頂點5到頂點1的最短路徑假設是5->4->2->1,那麼,頂點4到1的最短路徑一定是4->2->1,頂點2到1的最短路徑一定是2->1。這種性質就叫做最優子結構性質。
問題的最優子結構性質是該問題可用動態規劃算法或貪心算法求解的關鍵特徵。

單源最短路徑

Dijkstra算法是解單源最短路徑問題的貪心算法。
算法思想的簡單描述:要找出源到其他頂點的最短距離,首先將所有頂點劃分成兩個集合,S是已經到達的頂點,V是沒有到達的頂點,顯然S+V就是所有頂點。初始,S集合中只包含源頂點(本例中是1號頂點),然後找出V中距離S集合最近的一個頂點(即貪心選擇,至於爲什麼是S集合,請務必理解該問題的最優子結構性質)。那麼顯然,需要有個對象來記錄每個頂點到源頂點的距離,這就是代碼中的dist數組。dist[2]=x就表示,頂點2到源頂點的最短距離是x。找到後,記錄下路徑。如何記錄,同樣需要一個對象,即代碼中的prev數組。prev[2]=y就表示,頂點2到源頂點的最短路徑中,頂點2的前一個頂點是頂點y。是不是和鏈表有點像?記錄下路徑的同時是不是還需要將該頂點加入到S集合中呢?它就是s數組了。由於簡單,不多說。

測試數據:

帶權有向圖

代碼:

public class Dijkstra {
    static float max = Float.MAX_VALUE;

    /**
     * 
     * @param v 源
     * @param a 圖
     * @param dist 路徑長度
     * @param prev 路徑
     */
    public static void dijkstra(int v, float[][] a, float[] dist, int[] prev) {
        // v是源,dist[i]表示當前從源到頂點i的最短特殊路徑長度,prev[i]=j:最短路徑中頂點i的前一個頂點是j,類似於鏈表
        int n = dist.length - 1;// 節點個數
        if (v < 1 || v > n)
            return;
        boolean[] s = new boolean[n + 1];
        // 初始化
        for (int i = 1; i <= n; i++) {
            dist[i] = a[v][i];
            s[i] = false;
            if (dist[i] == Float.MAX_VALUE)
                prev[i] = 0;
            else
                prev[i] = v;
        }
        dist[v] = 0;
        s[v] = true;

        for (int i = 1; i < n; i++) {// 循環n-1次
            float temp = Float.MAX_VALUE;
            int u = v;
            for (int j = 1; j <= n; j++) {// 尋找不在集合內且距離集合最近的節點j
                if ((!s[j]) && (dist[j] < temp)) {
                    u = j;// 記錄節點
                    temp = dist[j];// 記錄最短特殊路徑長度
                }
            }

            s[u] = true;// 將節點u放入集合

            for (int j = 1; j <= n; j++) {// 重新設置dist[]和prev[]的值
                if ((!s[j]) && (a[u][j] < Float.MAX_VALUE)) {// 尋找不在集合內,且可達的節點
                    float newdist = dist[u] + a[u][j];
                    if (newdist < dist[j]) { // 與舊值進行比較,保留小的值
                        dist[j] = newdist;
                        prev[j] = u;
                    }
                }
            }
        }
    }

    public static void main(String[] args) {
        float[][] a = { { max, max, max, max, max, max },
                { max, 0, 10, max, 30, 100 }, { max, max, 0, 50, max, max },
                { max, max, max, 0, max, 10 }, { max, max, max, 20, 0, 60 },
                { max, max, max, max, max, 0 } };
        int n = a.length;
        float[] dist = new float[n];
        int[] prev = new int[n];
        dijkstra(1, a, dist, prev);
        System.out.println(" 頂點1到5的最短路徑:");
        trace(prev, 5);
        System.out.println();
        System.out.println(" 頂點1到3的最短路徑:");
        trace(prev, 3);
    }

    public static void trace(int[] prev, int n) {
        if (n == 1) {
            System.out.print(n + " ");
            return;
        }
        trace(prev, prev[n]);
        System.out.print(n + " ");
    }
}

測試數據運行結果:

 頂點1到5的最短路徑:
1 4 3 5 
 頂點1到3的最短路徑:
1 4 3 

將其中的一些運算步驟打印出來後,如下:
其中,prev數組記錄路徑。如果要找出頂點1到5的最短路徑,可以從數組prev得到頂點5的前一個頂點是3,3的前一個訂單是4,4的前一個頂點是1。於是從頂點1到5的最短路徑爲1,4,3,5.
dist數組記錄當前頂點距離源的最短路徑長度。
s[1]=true表示1頂點已經計算出最短路徑了,不需要再計算了。

初始化:
                   1         2         3         4         5
    prev[]         1         1         0         1         1
    dist[]         0        10       max        30       100
       s[]      true     false     false     false     false

對數組進行必要的修改:
                   1         2         3         4         5
    prev[]         1         1         2         1         1
    dist[]         0        10        60        30       100
       s[]      true      true     false     false     false

對數組進行必要的修改:
                   1         2         3         4         5
    prev[]         1         1         4         1         4
    dist[]         0        10        50        30        90
       s[]      true      true     false      true     false

對數組進行必要的修改:
                   1         2         3         4         5
    prev[]         1         1         4         1         3
    dist[]         0        10        50        30        60
       s[]      true      true      true      true     false

對數組進行必要的修改:
                   1         2         3         4         5
    prev[]         1         1         4         1         3
    dist[]         0        10        50        30        60
       s[]      true      true      true      true      true

最小生成樹

1. Prim算法

算法的簡單描述:與單源最短路徑類似。同樣將頂點分成兩個集合,S和V。初始,S集合只包含頂點1,然後找出V集合中距離S集合距離最短的頂點,所以同樣需要有對象保存頂點到集合S的距離,即lowcost數組。lowcost[2]=x就表示,頂點2距離集合S的最短距離是x。找到後,需要記錄路徑,即closest數組。closest[2]=y就表示,頂點2距離集合S中最近的頂點是y。同樣,也需要一個變量來表示一個頂點是屬於哪個集合,即s數組。

測試數據:

連通帶權圖

代碼:

public class Prim {
    /**
     * @param n 圖頂點個數
     * @param c 圖的二維數組
     */
    public static void prim(int n,float [][] c){
        float [] lowcost=new float [n+1];
        int [] closest=new int [n+1];
        boolean [] s=new boolean[n+1];

        //初始化
        s[1]=true;      //以第一個節點爲起點
        for(int i=2;i<=n;i++){
            lowcost[i]=c[1][i];
            closest[i]=1;
            s[i]=false;
        }

        for(int i=1;i<n;i++){   //循環n-1次找出剩餘n-1個節點
            float min=Float.MAX_VALUE;
            int j=1;
            //找集合外與集合最近的節點
            for(int k=2;k<=n;k++){
                if((lowcost[k]<min)&&(!s[k])){
                    min=lowcost[k];
                    j=k;
                }
            }
            System.out.println("找到邊"+j+","+closest[j]);

            s[j]=true;
            //找離j最近的節點k,尋找與集合最近的節點
            for(int k=2;k<=n;k++){
                if((c[j][k]<lowcost[k])&&(!s[k])){
                    lowcost[k]=c[j][k];     //記錄權值
                    closest[k]=j;   //記錄節點
                }
            }
        }
    }

    public static void main(String[] args){
        float [][] c={
                {100,100,100,100,100,100,100},
                {100,100,6,1,5,100,100},
                {100,6,100,5,100,3,100},
                {100,1,5,100,5,6,4},
                {100,5,100,5,100,100,2},
                {100,100,3,6,100,100,6},
                {100,100,100,4,2,6,100}};
        prim(6,c);
    }
}

打印運算時的中間數據,如下:

初始化:
                   1         2         3         4         5         6
 lowcost[]         0         6         1         5       100       100
 closest[]         0         1         1         1         1         1
       s[]      true     false     false     false     false     false
找到邊3,1
對數組進行必要的修改:
                   1         2         3         4         5         6
 lowcost[]         0         5         1         5         6         4
 closest[]         0         3         1         1         3         3
       s[]      true     false      true     false     false     false
找到邊6,3
對數組進行必要的修改:
                   1         2         3         4         5         6
 lowcost[]         0         5         1         2         6         4
 closest[]         0         3         1         6         3         3
       s[]      true     false      true     false     false      true
找到邊4,6
對數組進行必要的修改:
                   1         2         3         4         5         6
 lowcost[]         0         5         1         2         6         4
 closest[]         0         3         1         6         3         3
       s[]      true     false      true      true     false      true
找到邊2,3
對數組進行必要的修改:
                   1         2         3         4         5         6
 lowcost[]         0         5         1         2         3         4
 closest[]         0         3         1         6         2         3
       s[]      true      true      true      true     false      true
找到邊5,2
對數組進行必要的修改:
                   1         2         3         4         5         6
 lowcost[]         0         5         1         2         3         4
 closest[]         0         3         1         6         2         3
       s[]      true      true      true      true      true      true

2. Kruskal算法

Kruskal算法是構造最小生成樹的另一個常用算法。
(這個大坑先留着)

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