1. 是什麼?
克魯斯卡爾算法其實也是生成最小生成樹的一種算法,和普里姆算法一樣,解決同一類問題的。
有7個公交站(A, B, C, D, E, F, G) ,現在需要修路把7個公交站連通,各個公交站之間的距離如下。問如何修路,能使各個公交站連通且修路的總里程數最小?
這個和上次說到的普里姆算法修路問題是一樣的,下面來看看用克魯斯卡爾算法怎麼解決。
2. 算法步驟:
首先對邊的權值進行排序,選擇權值最小的一條邊,即EF;
選擇權值第二小的邊,即CD;
選擇權值第三小的邊,即DE;
選擇權值第四小的邊,即CE,但是,如果選擇CE,那就形成迴路了。什麼叫回路呢?CDE這個三角形三條邊都修了路,那就是迴路。所以不能選擇CE,那就嘗試選擇第五小的邊,即CF,但是CF也不能選,如果選了,四邊形CFED就形成迴路了。所以繼續選擇第六小的邊,BF,這個是可以選擇的;
接下來依次選擇EG、AB。7個頂點總共要選擇6條邊,這6條邊分別是:EF、CD、DE、BF、EG、AB。
克魯斯卡爾算法的難點在於,怎麼判斷選擇了這條邊是否會形成迴路。
- 判斷是否形成迴路的方法:記錄頂點在最小生成樹中的終點,頂點的終點是“在最小生成樹中與它連通的最大頂點”。然後每次需要將一條邊添加到最小生成樹時,判斷該邊的兩個頂點終點是否相同,相同就會構成迴路。
看了這段話,可能還是一臉蒙逼,還是以上面的圖爲例,看步驟:
首先ABCDEFG這7個頂點,在頂點集合中應該是按照順序存放的;
按照上面的分析,第一次選擇的是EF,毫無疑問這一條邊的終點是F;
第二次選擇的CD的終點D;
第三次選擇的DE,終點是F,因爲此時D和E相連,D又和F相連,所以D的終點是F。而且,因爲C和D是相連的,D和E相連,E和F也是相連的,所以C的終點此時變成了F。也就是說,當選擇了EF、CD、DE這三條邊後,C、D、E的終點都是F。當然F的終點也是F,因爲F還沒和後面的哪個頂點連接。
本來接下來應該選擇CE的,但是由於C和E的終點都是F,所以就會形成迴路。
3. 代碼實現:
首先還是定義一個類,用來表示圖,如下:
/**
* 圖
* @author zhu
*
*/
class Graph{
List<String> vertexs; // 存放頂點
int[][] edges; // 鄰接矩陣,存放邊
public Graph(List<String> vertexs, int[][] edges) {
this.vertexs = vertexs;
this.edges = edges;
}
}
然後,再定義一個最小生成樹類,寫上一些基礎方法,比如生成圖,打印圖的鄰接矩陣等,如下:
/**
* 最小生成樹
* @author zhu
*
*/
class MinTree{
/**
* 創建圖
* @param vertex 頂點集合
* @param edges 鄰接矩陣
*/
public Graph createGraph(List<String> vertex, int[][] edges) {
return new Graph(vertex, edges);
}
/**
* 打印圖的二維數組
* @param graph
*/
public void printGraph(Graph graph) {
int[][] edges = graph.edges;
for (int i=0; i<edges.length; i++) {
System.out.println(Arrays.toString(edges[i]));
}
}
}
因爲我們上面的分析說了,要對邊進行排序。現在邊是保存在鄰接矩陣中的,不太好處理,所以再定義一個類,用來表示邊:
/**
* 邊的對象
* @author zhu
*
*/
class EdgeObject{
String start; // 邊的端點
String end; // 邊的另一個端點
int weight; // 邊的權值
public EdgeObject(String start, String end, int weight) {
this.start = start;
this.end = end;
this.weight = weight;
}
@Override
public String toString() {
return "EdgeObject [start=" + start + ", end=" + end + ", weight=" + weight + "]";
}
}
有了這個類來表示邊,接下來就可以寫一個方法,將圖的鄰接矩陣轉成邊對象,即在MinTree類中加如下方法:
/**
* 將圖的邊,即鄰接矩陣,轉成邊對象
* @param graph
* @return
*/
public List<EdgeObject> transfer2Object(Graph graph){
List<EdgeObject> edgeList = new ArrayList<>();
for (int i=0; i<graph.edges.length; i++) {
for (int j=i+1; j<graph.edges[0].length; j++) {
if (graph.edges[i][j] != 100) {
EdgeObject edgeObject = new EdgeObject(graph.vertexs.get(i), graph.vertexs.get(j), graph.edges[i][j]);
edgeList.add(edgeObject);
}
}
}
return edgeList;
}
獲取到了邊對象的集合,就可以對其進行排序了,所以中MinTree類中增加一個排序的方法:
/**
* 對邊進行排序
* @param edgeObject
*/
public void sortEdges(List<EdgeObject> edgeObjects) {
Collections.sort(edgeObjects, new Comparator<EdgeObject>() {
@Override
public int compare(EdgeObject o1, EdgeObject o2) {
return o1.weight - o2.weight;
}
});
}
至此,對邊進行排序的準備工作就做完了。接下來還需要中MinTree類中新增兩個方法,即實現克魯斯卡爾算法的方法,如下:
/**
* 獲取索引爲i的頂點的終點
* @param endArray 存放終點的數組
* @param i 傳入頂點的索引
* @return 頂點i的終點對應的索引
*/
public int getEnd(int[] endArray, int i) {
while (endArray[i] != 0) {
i = endArray[i];
}
return i;
}
/**
* 克魯斯卡爾算法創建最小生成樹
* @param graph 圖
* @param currentVertex 開始處理的頂點
*/
public List<EdgeObject> kruskal(Graph graph) {
// 最終選擇的邊的集合
List<EdgeObject> resultList = new ArrayList<>();
// 用於保存終點的數組
int[] ends = new int[graph.vertexs.size()];
// 將圖的鄰接矩陣轉成邊對象集合
List<EdgeObject> list = transfer2Object(graph);
// 對邊進行排序
sortEdges(list);
// 遍歷邊的集合
for (EdgeObject e : list) {
int p1 = graph.vertexs.indexOf(e.start); // 邊的第一個頂點的索引
int p2 = graph.vertexs.indexOf(e.end); // 邊的第二個頂點的索引
int m = getEnd(ends, p1); // 獲取p1的終點
int n = getEnd(ends, p2); // 獲取p2的終點
if (m != n) { // 如果這兩個頂點的終點相同,就會構成迴路,反之就可以添加這條邊
ends[m] = n; // 將索引爲m的頂點的終點設置爲索引爲n的頂點
resultList.add(e); // 將這條邊加入到保存結果的集合中
}
}
return resultList;
}
解釋一下這兩個方法,首先看第二個方法:
首先定義保存最終結果的集合;
然後定一個了一個數組,用來保存終點。數組的大小就是圖的頂點的個數。下標表示的是頂點,比如下標0,那代表的就是A這個頂點,下標1代表的就是B這個頂點;下標對應的值表示的是該下標對應的頂點的終點的索引。比如
ends[0] = 1
,表示的是A的終點是B。再然後調用上面的方法,將鄰接矩陣轉成邊對象的集合,並且進行排序;
接着遍歷這個邊的集合,每拿到一條邊,就判斷這條邊的兩個端點的終點是否相同。比如第一次拿到的邊是EF,那麼
p1 = 4, p2 = 5
。接下來用getEnd方法去獲取終點。獲取p1終點的時候,保存終點的ends數組中還全部都是0,所以ends[p1] = 0
,不進入while循環,直接return了p1的值,即m = 4
;同理n = 5
。m不等於n,這時,就讓ends[4] = 5
,4對應的是E,5對應的是F,這句話的作用就是將E的終點設置爲了F。拿到的第二條邊應該是CD,
p1 = 2, p2 = 3
,m = 2, n = 3
,ends[2] = 3
,將C的終點設置成了D。拿到的第三條邊是DE,
p1 = 3, p2 = 4
,因爲ends[3]、ends[4] = 0
,不會進入while循環,所以m = 3, n = 4
,ends[3] = 4
,將D的終點設置成了E。拿到的第四條邊是CE,
p1 = 2, p2 = 4
,此時,ends[2] = 3 != 0
,所以進入while循環,ends[3] = 4 != 0, ends[4] = 5 != 0, ends[5] = 0
,所以m = 5
,也就是說,C的終點是索引5對應的,即F;接着看n等於多少。ends[4] = 5 != 0
,進入while循環,ends[5] = 0
,所以n = 5
,此時m和n相等,說明終點是同一個,都是F,所以不添加這條邊。
……
這裏就是getEnd這個方法不太好理解,調試一下就很清晰了。