一個(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(普里姆)算法
算法思路:
- 定義一個(gè)臨時(shí)的一維數(shù)組,用于存放可用的連接邊,數(shù)組下標(biāo)為頂點(diǎn)序號(hào),值為權(quán)值
- 任選一個(gè)點(diǎn)作為起點(diǎn),以起點(diǎn)的所有權(quán)值對(duì)數(shù)組進(jìn)行初始化
- 找出數(shù)組中最小權(quán)值的邊,即為最小生成樹中的一條有效邊
- 將找到的最小邊在數(shù)組中賦值為0,代表已經(jīng)使用過。并將數(shù)組與找到頂點(diǎn)的所有邊進(jìn)行比較,若頂點(diǎn)的邊的權(quán)值比當(dāng)前數(shù)組存放的可用邊的權(quán)值小,則進(jìn)行覆蓋
- 重復(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(克魯斯卡爾)算法
算法思路:
- 現(xiàn)將所有邊進(jìn)行權(quán)值的從小到大排序
- 定義一個(gè)一維數(shù)組代表連接過的邊,數(shù)組的下標(biāo)為邊的起點(diǎn),值為邊的終點(diǎn)
- 按照排好序的集合用邊對(duì)頂點(diǎn)進(jìn)行依次連接,連接的邊則存放到一維數(shù)組中
- 用一維數(shù)組判斷是否對(duì)已經(jīng)連接的邊能構(gòu)成回路,有回路則無效,沒回路則是一條有效邊
- 重復(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流程圖