克魯斯卡爾算法(公交站問題)

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 = 3m = 2, n = 3ends[2] = 3,將C的終點設置成了D。

  • 拿到的第三條邊是DE,p1 = 3, p2 = 4,因爲ends[3]、ends[4] = 0,不會進入while循環,所以m = 3, n = 4ends[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這個方法不太好理解,調試一下就很清晰了。

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