貪心演算法解決單源最短路徑問題
參考教材:演算法設計與分析(第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演算法是構造最小生成樹的另一個常用演算法。
(這個大坑先留著)