Java 圖的最小生成樹 — prim算法和kruskal算法

一個(gè)有 n 個(gè)結(jié)點(diǎn)的連通圖的生成樹是原圖的極小連通子圖,且包含原圖中的所有 n 個(gè)結(jié)點(diǎn),并且有保持圖連通的權(quán)值和邊最小

一、最小生成樹的應(yīng)用

生成樹和最小生成樹有許多重要的應(yīng)用。

例如:要在n個(gè)城市之間鋪設(shè)光纜,主要目標(biāo)是要使這 n 個(gè)城市的任意兩個(gè)之間都可以通信,但鋪設(shè)光纜的費(fèi)用很高,且各個(gè)城市之間鋪設(shè)光纜的費(fèi)用不同,因此另一個(gè)目標(biāo)是要使鋪設(shè)光纜的總費(fèi)用最低。這就需要找到帶權(quán)值的最小生成樹。

構(gòu)建最小生成樹時(shí)最常用的方法就是prim算法和kruskal算法

二、圖的入度和出度

1.構(gòu)建圖的鄰接矩陣

以上圖中的圖結(jié)構(gòu)為例,由于圖是頂點(diǎn)與頂點(diǎn)之間的連接關(guān)系,又帶有權(quán)值,所以我們可以用鄰接矩陣來表示圖中頂點(diǎn)的關(guān)系

圖的鄰接矩陣
  • 矩陣中的值代表頂點(diǎn)與頂點(diǎn)之間的權(quán)值,由于示例是一個(gè)無向圖,所以這個(gè)矩陣是以對(duì)角線對(duì)稱的
  • 我們可以將矩陣看成一個(gè)二維數(shù)組,因此就可以很容易的創(chuàng)建出這個(gè)圖的數(shù)據(jù)結(jié)構(gòu)了
int[] graph0 = new int[]{0, 10, MAX_WEIGHT, MAX_WEIGHT, MAX_WEIGHT, 11, MAX_WEIGHT, MAX_WEIGHT, MAX_WEIGHT};
int[] graph1 = new int[]{10, 0, 18, MAX_WEIGHT, MAX_WEIGHT, MAX_WEIGHT, 16, MAX_WEIGHT, 12};
int[] graph2 = new int[]{MAX_WEIGHT, 18, 0, 22, MAX_WEIGHT, MAX_WEIGHT, MAX_WEIGHT, MAX_WEIGHT, 8};
int[] graph3 = new int[]{MAX_WEIGHT, MAX_WEIGHT, 22, 0, 20, MAX_WEIGHT, MAX_WEIGHT, 16, 21};
int[] graph4 = new int[]{MAX_WEIGHT, MAX_WEIGHT, MAX_WEIGHT, 20, 0, 26, MAX_WEIGHT, 7, MAX_WEIGHT};
int[] graph5 = new int[]{11, MAX_WEIGHT, MAX_WEIGHT, MAX_WEIGHT, 26, 0, 17, MAX_WEIGHT, MAX_WEIGHT};
int[] graph6 = new int[]{MAX_WEIGHT, 16, MAX_WEIGHT, MAX_WEIGHT, MAX_WEIGHT, 17, 0, 19, MAX_WEIGHT};
int[] graph7 = new int[]{MAX_WEIGHT, MAX_WEIGHT, MAX_WEIGHT, 16, 7, MAX_WEIGHT, 19, 0, MAX_WEIGHT};
int[] graph8 = new int[]{MAX_WEIGHT, 12, 8, 21, MAX_WEIGHT, MAX_WEIGHT, MAX_WEIGHT, MAX_WEIGHT, 0};

2.入度與出度

  • 頂點(diǎn)的出邊條數(shù)稱為該頂點(diǎn)的出度
  • 頂點(diǎn)的入邊條數(shù)稱為該項(xiàng)點(diǎn)的入度
  • 則在矩陣中某個(gè)點(diǎn)的入度和出度即為橫向和縱向的有效權(quán)值個(gè)數(shù)
/**
 * 獲取某個(gè)頂點(diǎn)的出度
 *
 * @param index 頂點(diǎn)序號(hào)
 * @return 出度
 */
public int getOutDegree(int index) {
    int degree = 0;
    for (int i = 0; i < vertexSize; i++) {
        int weight = matrix[index][i];
        if (weight > 0 && weight < MAX_WEIGHT) {
            degree++;
        }
    }
    return degree;
}

/**
 * 獲取某個(gè)頂點(diǎn)的入度
 *
 * @param index 頂點(diǎn)序號(hào)
 * @return 入度
 */
public int getInDegree(int index) {
    int degree = 0;
    for (int i = 0; i < vertexSize; i++) {
        int weight = matrix[i][index];
        if (weight > 0 && weight < MAX_WEIGHT) {
            degree++;
        }
    }
    return degree;
}

三、prim(普里姆)算法

算法思路:

  1. 定義一個(gè)臨時(shí)的一維數(shù)組,用于存放可用的連接邊,數(shù)組下標(biāo)為頂點(diǎn)序號(hào),值為權(quán)值
  2. 任選一個(gè)點(diǎn)作為起點(diǎn),以起點(diǎn)的所有權(quán)值對(duì)數(shù)組進(jìn)行初始化
  3. 找出數(shù)組中最小權(quán)值的邊,即為最小生成樹中的一條有效邊
  4. 將找到的最小邊在數(shù)組中賦值為0,代表已經(jīng)使用過。并將數(shù)組與找到頂點(diǎn)的所有邊進(jìn)行比較,若頂點(diǎn)的邊的權(quán)值比當(dāng)前數(shù)組存放的可用邊的權(quán)值小,則進(jìn)行覆蓋
  5. 重復(fù)循環(huán)2,3,4的操作直至遍歷完所有頂點(diǎn)

算法代碼:

/**
 * 最小生成樹,普里姆(prim)算法
 */
public void createMinSpanTreePrim() {
    // 定義一維數(shù)組,存放用于比較最小權(quán)值的頂點(diǎn)權(quán)值,0代表已經(jīng)比較過
    int[] lowcost = new int[vertexSize];
    
    // 初始化數(shù)組為第一個(gè)頂點(diǎn)的權(quán)值
    System.arraycopy(matrix[0], 0, lowcost, 0, vertexSize);
    
    int sum = 0;
    // 循環(huán)比較
    for (int i = 0; i < vertexSize; i++) {
    
        // 先比較找出最小的權(quán)值節(jié)點(diǎn)
        int min = -1;
        for (int j = 0; j < vertexSize; j++) {
            if (lowcost[j] > 0 && lowcost[j] < MAX_WEIGHT) {
                if (min == -1 || lowcost[min] > lowcost[j]) {
                    min = j;
                }
            }
        }
    
        // 判斷是否全部為0,找不到最小值
        if (min == -1) {
            break;
        }
    
        System.out.println("訪問到了節(jié)點(diǎn):" + min + ",權(quán)值:" + lowcost[min]);
        sum += lowcost[min];
    
        // 將當(dāng)前節(jié)點(diǎn)的值修改成0
        lowcost[min] = 0;
    
        // 將存放最小權(quán)值的數(shù)組與下一個(gè)節(jié)點(diǎn)的所有連接點(diǎn)對(duì)比,找出最小權(quán)值
        for (int j = 0; j < vertexSize; j++) {
            if (matrix[min][j] < lowcost[j]) {
                lowcost[j] = matrix[min][j];
            }
        }
    }
    System.out.println("最小生成樹的權(quán)值總和:" + sum);
}

下圖是畫的一個(gè)針對(duì)此代碼運(yùn)行的流程圖(畫了半天發(fā)現(xiàn)在文章中顯示的比較小,如果看起來覺得小的同學(xué)可以下載原圖后放大觀看)

  • 綠色代表一維數(shù)組中存放的可用最小邊
  • 紅色代表找到的最小生成樹的邊
prim流程圖

四、kruskal(克魯斯卡爾)算法

算法思路:

  1. 現(xiàn)將所有邊進(jìn)行權(quán)值的從小到大排序
  2. 定義一個(gè)一維數(shù)組代表連接過的邊,數(shù)組的下標(biāo)為邊的起點(diǎn),值為邊的終點(diǎn)
  3. 按照排好序的集合用邊對(duì)頂點(diǎn)進(jìn)行依次連接,連接的邊則存放到一維數(shù)組中
  4. 用一維數(shù)組判斷是否對(duì)已經(jīng)連接的邊能構(gòu)成回路,有回路則無效,沒回路則是一條有效邊
  5. 重復(fù)3,4直至遍歷完所有的邊為止,即找到最小生成樹

首先將所有邊按權(quán)值進(jìn)行排序

kruskal算法

定義一個(gè)邊的對(duì)象

/**
 * 連接頂點(diǎn)的邊
 */
class Edge {

    private int start;
    private int end;
    private int weight;

    public Edge(int start, int end, int weight) {
        this.start = start;
        this.end = end;
        this.weight = weight;
    }
}

按照排序的圖對(duì)邊進(jìn)行初始化

Edge edge0 = new Edge(4, 7, 7);
Edge edge1 = new Edge(2, 8, 8);
Edge edge2 = new Edge(0, 1, 10);
Edge edge3 = new Edge(0, 5, 11);
Edge edge4 = new Edge(1, 8, 12);
Edge edge5 = new Edge(3, 7, 16);
Edge edge6 = new Edge(1, 6, 16);
Edge edge7 = new Edge(5, 6, 17);
Edge edge8 = new Edge(1, 2, 18);
Edge edge9 = new Edge(6, 7, 19);
Edge edge10 = new Edge(3, 4, 20);
Edge edge11 = new Edge(3, 8, 21);
Edge edge12 = new Edge(2, 3, 22);
Edge edge13 = new Edge(3, 6, 24);
Edge edge14 = new Edge(4, 5, 26);

最小生成樹算法代碼:

/**
 * kruskal算法創(chuàng)建最小生成樹
 */
public void createMinSpanTreeKruskal() {
    // 定義一個(gè)一維數(shù)組,下標(biāo)為連線的起點(diǎn),值為連線的終點(diǎn)
    int[] parent = new int[edgeSize];
    for (int i = 0; i < edgeSize; i++) {
        parent[i] = 0;
    }

    int sum = 0;
    for (Edge edge : edges) {

        // 找到起點(diǎn)和終點(diǎn)在臨時(shí)連線數(shù)組中的最后連接點(diǎn)
        int start = find(parent, edge.start);
        int end = find(parent, edge.end);

        // 通過起點(diǎn)和終點(diǎn)找到的最后連接點(diǎn)是否為同一個(gè)點(diǎn),是則產(chǎn)生回環(huán)
        if (start != end) {

            // 沒有產(chǎn)生回環(huán)則將臨時(shí)數(shù)組中,起點(diǎn)為下標(biāo),終點(diǎn)為值
            parent[start] = end;
            System.out.println("訪問到了節(jié)點(diǎn):{" + start + "," + end + "},權(quán)值:" + edge.weight);
            sum += edge.weight;
        }
    }
    System.out.println("最小生成樹的權(quán)值總和:" + sum);
}


/**
 * 獲取集合的最后節(jié)點(diǎn)
 */
private int find(int parent[], int index) {
    while (parent[index] > 0) {
        index = parent[index];
    }
    return index;
}

下圖是畫的一個(gè)針對(duì)此代碼運(yùn)行的流程圖

  • 紅色代表找到的最小生成樹的邊
  • 藍(lán)色代表找到的邊但是是回環(huán)則無效
kruskal流程圖

源碼地址

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容