應用場景-修路問題
勝利鄉有 7 個村庄(A, B, C, D, E, F, G
) ,現在需要修路把 7 個村庄連通,各個 村庄的距離用邊線表示(權) ,比如 A – B 距離 5 公里
問:如何修路保證各個村庄都能連通,並且總的修建公路 總里程最短?
思路:
- 只滿足連通:將 10 條邊,連接即可,但是總的里程數不是最小.
- 滿足連通,又保證總里程最短:就是盡可能的選擇少的路線,並且每條路線最小,保證總里程數最少
最小生成樹
修路問題本質就是就是 最小生成樹問題, 最小生成樹(Minimum Cost Spanning Tree),簡稱 MST。
給定一個 帶權的無向連通圖,如何選取一棵生成樹,使樹上所有邊上權的總和為最小,這叫最小生成樹 ,它有如下特點:
- N 個頂點,一定有
N-1
條邊 - 包含全部頂點
N-1
條邊都在圖中
比如下圖:
求最小生成樹的算法主要是 普里姆算法(prim) 和 克魯斯卡爾算法(kruskal)
普利姆算法(prim)介紹
普利姆(Prim)算法求最小生成樹,也就是:在包含 n 個頂點的連通圖中,找出只有(n-1
)條邊包含所有 n 個頂點的連通子圖,也就是所謂的 極小連通子圖
它的算法如下:
- 設
G=(V,E)
是連通網T=(U,D)
是最小生成樹- V、U 是頂點集合
- E、D 是邊的集合
- 若從頂點 u 開始構造最小生成樹,則從集合 V 中取出頂點 u 放入集合 U 中,標記頂點 v 的
visited[u]=1
(說明該點已經被連接) - 若集合 U 中頂點 ui 與集合 V-U 中的頂點 vj 之間存在邊,則尋找這些邊中權值最小的邊,但不能構成回路,將頂點 vj 加入集合U中,將邊(ui,vj)加入集合 D 中,標記
visited[vj]=1
- 重復步驟 ②,直到 U 與 V 相等,即所有頂點都被標記為訪問過,此時 D 中有 n-1 條邊
上面說的可能有點抽象難懂,不過沒事,請繼續往下看,就懂了。
普利姆算法圖解
以這個為例子:
-
從 A 點開始處理
與 A 直連的點有:
A,C [7]
:后面中括號中的是邊的權值A,B [5]
A,G [2]
在這個所有的邊中,
A,G [2]
的權值最小,那么結果是:A、G
-
從
A、G
開始,找到他們的直連邊,但是不包含已經訪選擇過的邊。A,C [7]
A,B [5]
G,B [3]
G,E [4]
G,F [6]
在以上邊中,權值最小的是:
G,B [3]
,那么結果是:A、G、B
-
以
A、G、B
開始,找到他們的直連邊,但是不包含已經訪選擇過的邊。A,C [7]
A,B [5]
G,E [4]
G,F [6]
B,D [9]
在以上邊中,權值最小的是:
G,E [4]
,那么結果是:A、G、B、E
以此類推
-
A、G、B、E
→ 權值最小的邊是E,F [5]
→A、G、B、E、F
-
A、G、B、E、F
→ 權值最小的邊是F,D [4]
→A、G、B、E、F、D
-
A、G、B、E、F、D
→ 權值最小的邊是A,C [7]
→A、G、B、E、F、D、C
那么最終結果則為下圖:
總里程數量為:2+3+4+5+4+7=25
動圖:
代碼實現
成圖
/**
* 普利姆算法
*/
public class PrimAlgorithm {
/**
* 圖:首先需要有一個帶權的連通無向圖
*/
class MGraph {
int vertex; // 頂點個數
int[][] weights; // 鄰接矩陣
char[] datas; // 村庄數據
/**
* @param vertex 村庄數量, 會按照數量,按順序生成村庄,如 A、B、C...
* @param weights 需要你自己定義好那些點是連通的,那些不是連通的
*/
public MGraph(int vertex, int[][] weights) {
this.vertex = vertex;
this.weights = weights;
this.datas = new char[vertex];
for (int i = 0; i < vertex; i++) {
// 大寫字母 A 從 65 開始
datas[i] = (char) (65 + i);
}
}
public void show() {
System.out.printf("%-8s"," ");
for (char vertex : datas) {
// 控制字符串輸出長度:少於 8 位的,右側用空格補位
System.out.printf("%-8s", vertex + " ");
}
System.out.println();
for (int i = 0; i < weights.length; i++) {
System.out.printf("%-8s", datas[i] + " ");
for (int j = 0; j < weights.length; j++) {
System.out.printf("%-8s", weights[i][j] + " ");
}
System.out.println();
}
}
}
@Test
public void mGraphTest() {
// 不連通的默認值:
// 這里設置為較大的數,是為了后續的計算方便,計算權值的時候,不會選擇
int defaultNo = 100000;
int[][] weights = new int[][]{
{defaultNo, 5, 7, defaultNo, defaultNo, defaultNo, 2}, // A
{5, defaultNo, defaultNo, 9, defaultNo, defaultNo, 3},// B
{7, defaultNo, defaultNo, defaultNo, 8, defaultNo, defaultNo},// C
{defaultNo, 9, defaultNo, defaultNo, defaultNo, 4, defaultNo},// D
{defaultNo, defaultNo, 8, defaultNo, defaultNo, 5, 4},// E
{defaultNo, defaultNo, defaultNo, 4, 5, defaultNo, 6},// F
{2, 3, defaultNo, defaultNo, 4, 6, defaultNo}// G
};
MGraph mGraph = new MGraph(7, weights);
mGraph.show();
}
}
測試輸出
A B C D E F G
A 100000 5 7 100000 100000 100000 2
B 5 100000 100000 9 100000 100000 3
C 7 100000 100000 100000 8 100000 100000
D 100000 9 100000 100000 100000 4 100000
E 100000 100000 8 100000 100000 5 4
F 100000 100000 100000 4 5 100000 6
G 2 3 100000 100000 4 6 100000
使用鄰接矩陣構建了一個無向圖,按照題目中的連通要求進行構建。
最小生成樹
/**
* 最小生成樹
*/
class MinTree {
/**
* 普利姆算法
*
* @param mGraph 無向圖
* @param v 從哪一個點開始生成
*/
public void prim(MGraph mGraph, int v) {
int minTotalWeight = 0; // 記錄已選擇的邊的總權值,僅僅只是為了測試打印驗證,不包含在算法中
// 記錄已經選擇過的節點
boolean[] selects = new boolean[mGraph.vertex];
// 從哪個節點開始,則標識已經被訪問
selects[v] = true;
// 一共要生成 n-1 條邊
for (int i = 1; i < mGraph.vertex; i++) {
// 每次循環:選擇一條權值最小的邊出來
// 記錄最小值
int minWeight = 10000;
//村庄索引
int x = -1;//已經訪問過的村庄索引
int y = -1;//剛建立連接的村庄索引
// 每次查找權值最小的邊:根據思路,每次都是從已經選擇過的點,中去找與該點直連的點的權值
// 並且該點還沒有被選擇過:如果兩個點都被選擇過,要么他們是雙向的,要么就是被其他的點選擇過了
// 這里雙循環的含義:建議對照上面解析步驟分析理解
for (int j = 0; j < mGraph.vertex; j++) { //j的含義是已經訪問過的村庄索引,對照for循環體中的判斷語句理解
for (int k = 0; k < mGraph.vertex; k++) {//k的含義是未被訪問過的村庄索引,同樣對照for循環體中的判斷語句理解
// 通過 j 來限定已經選擇過的點
// 通過 k 來遍歷匹配,沒有選擇過的點
// 假設第一輪是 A 點:j = 0
// 那么在這里將會執行 0,1 0,2, 0,3 也就是與 A 點直連,且沒有被選擇過的點,的最小權值
if (selects[j] && !selects[k]
&& mGraph.weights[j][k] < minWeight
) {
// 記錄最小權值,與這條邊的信息
minWeight = mGraph.weights[j][k];
x = j;
y = k;
}
}
}
// 當一次循環結束時:就找到了一條權值最小的邊
System.out.printf("%s,%s [%s] \n", mGraph.datas[x], mGraph.datas[y], mGraph.weights[x][y]);
minTotalWeight += mGraph.weights[x][y]; // 統計已選擇邊權值,單純為了打印
//重置最小權值,必做步驟
minWeight = 10000;
// 記錄該點已經被選擇
// 在查找最小權值邊的代碼中:y=k, k 表示沒有被選擇過的點,所以,找到之后,這里記錄 y 為這條邊的另一個點,標記以訪問
selects[y] = true;
}
System.out.println("總權值:" + minTotalWeight);
}
}
/**
* 圖:首先需要有一個帶權的連通無向圖
*/
class MGraph {
int vertex; // 頂點個數
int[][] weights; // 鄰接矩陣
char[] datas; // 村庄數據
/**
* @param vertex 村庄數量, 會按照數量,按順序生成村庄,如 A、B、C...
* @param weights 需要你自己定義好那些點是連通的,那些不是連通的
*/
public MGraph(int vertex, int[][] weights) {
this.vertex = vertex;
this.weights = weights;
this.datas = new char[vertex];
for (int i = 0; i < vertex; i++) {
// 大寫字母 A 從 65 開始
datas[i] = (char) (65 + i);
}
}
public void show() {
System.out.printf("%-8s"," ");
for (char vertex : datas) {
// 控制字符串輸出長度:少於 8 位的,右側用空格補位
System.out.printf("%-8s", vertex + " ");
}
System.out.println();
for (int i = 0; i < weights.length; i++) {
System.out.printf("%-8s", datas[i] + " ");
for (int j = 0; j < weights.length; j++) {
System.out.printf("%-8s", weights[i][j] + " ");
}
System.out.println();
}
}
}
@Test
public void primTest() {
// 不連通的默認值:
// 這里設置為較大的數,是為了后續的計算方便,計算權值的時候,不會選擇
int defaultNo = 100000;
int[][] weights = new int[][]{
{defaultNo, 5, 7, defaultNo, defaultNo, defaultNo, 2}, // A
{5, defaultNo, defaultNo, 9, defaultNo, defaultNo, 3},// B
{7, defaultNo, defaultNo, defaultNo, 8, defaultNo, defaultNo},// C
{defaultNo, 9, defaultNo, defaultNo, defaultNo, 4, defaultNo},// D
{defaultNo, defaultNo, 8, defaultNo, defaultNo, 5, 4},// E
{defaultNo, defaultNo, defaultNo, 4, 5, defaultNo, 6},// F
{2, 3, defaultNo, defaultNo, 4, 6, defaultNo}// G
};
MGraph mGraph = new MGraph(7, weights);
mGraph.show();
MinTree minTree = new MinTree();
minTree.prim(mGraph, 0);
}
測試輸出
A B C D E F G
A 100000 5 7 100000 100000 100000 2
B 5 100000 100000 9 100000 100000 3
C 7 100000 100000 100000 8 100000 100000
D 100000 9 100000 100000 100000 4 100000
E 100000 100000 8 100000 100000 5 4
F 100000 100000 100000 4 5 100000 6
G 2 3 100000 100000 4 6 100000
A,G [2]
G,B [3]
G,E [4]
E,F [5]
F,D [4]
A,C [7]
總權值:25
測試結果也就是這個圖
動圖(從B點開始):
下面看看起始點不同的輸出信息,從 B 點開始:minTree.prim(mGraph, 1);
B,G [3]
G,A [2]
G,E [4]
E,F [5]
F,D [4]
A,C [7]
總權值:25
可以看到:順序不同,但是邊和總權值是相同的。