普里姆算法介紹:
普里姆算法(Prim算法),圖論中的一種算法,可在加權連通圖裏搜索最小生成樹。意即由此算法搜索到的邊子集所構成的樹中,不但包括了連通圖裏的所有頂點。
圖解過程:
算法思想:
- 一個加權連通圖,其中頂點集合爲V,邊集合爲E;
- 初始化:Vnew = {x},其中x爲集合V中的任一節點(起始點),Enew = {},爲空;
- 重複下列操作,直到Vnew = V:
a.在集合E中選取權值最小的邊<u, v>,其中u爲集合Vnew中的元素,而v不在Vnew集合當中,並且v∈V(如果存在有多條滿足前述條件即具有相同權值的邊,則可任意選取其中之一);
b.將v加入集合Vnew中,將<u, v>邊加入集合Enew中;
算法實現思路:
4. 首先,構造一個圖,此處我用矩陣表示,矩陣裏的節點值表示權值。(無邊時權值爲無窮大)
5. 選擇接收的第一個點爲起點,開始進行搜索。
6. 然後,我們要有一個數組(prims),保存每次所得到頂點,還要有一個數組(weights),存放起始點到其他每個頂點的權值,每加一個點都要改變一下,都要改變一下數組權值。
7. 遍歷所有頂點,在未被加入的點中,找出權值最小的,放入prims數組,將此點的權值改爲0,(表示已經加入了最小樹結果中,下次不再找這個點)。
8. 計算總的權值,遍歷prims頂點數組,從數組中找它前面的頂點到此頂點的權值的最小值,進行累加。
前期準備代碼(輸入圖的一些信息):
import java.io.IOException;
import java.util.Scanner;
public class Graph {
private int matrixNum;
private char[] vertex; //頂點集合
private int[][] matrix; //鄰接矩陣
private static final int INF = Integer.MAX_VALUE; //最大值
/**
* 創建圖
*/
public Graph() {
Scanner sca = new Scanner(System.in);
int vertexNum = sca.nextInt(); //頂點數
matrixNum = sca.nextInt(); //邊數
vertex = new char[vertexNum];
//初始化頂點
for(int i = 0; i < vertexNum; i++) {
vertex[i] = readChar();
}
//初始化 邊 的權值
matrix = new int [vertexNum][vertexNum];
for(int i = 0; i < vertexNum; i++)
for(int j = 0; j < vertexNum; j++)
matrix[i][j] = (i == j) ? 0 : INF;
//根據用戶輸入的邊的信息:初始化邊
for(int i = 0; i < matrixNum; i++) {
char start = readChar();
char end = readChar();
int weight = sca.nextInt();
int startInedx = getLocation(start);
int endIndex = getLocation(end);
if(startInedx == -1 || endIndex == -1) return;
matrix[startInedx][endIndex] = weight;
matrix[endIndex][startInedx] = weight;
}
sca.close();
}
/**
* 返回字符的位置
*/
private int getLocation(char c) {
for(int i = 0; i < vertex.length; i++)
if(vertex[i] == c) return i;
return -1;
}
/**
* 讀取一個輸入字符
* @return
*/
private char readChar() {
char ch = '0';
do {
try {
ch = (char)System.in.read();
} catch (IOException e) {
e.printStackTrace();
}
}while(!((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')));
return ch;
}
普里姆核心算法代碼:
/**
* prim算法
* @param start
*/
public void prim(int start) {
int num = vertex.length; //頂點個數
int index = 0; //prim最小的索引,即prims數組的索引
char[] prims = new char[num];//prim最小樹的結果
int[] weights = new int[num];//頂點間邊的權值
//prim最小生成樹中的第一個數是 "圖中第start個頂點",因爲是從strat開始的
prims[index++] = vertex[start];
//初始化"頂點的權值數組"
//將每個頂點的權值初始化爲第start個頂點 到"該頂點"的權值
for(int i = 0; i < num; i++)
weights[i] = matrix[start][i];
//自己到自己的路徑爲0
weights[start] = 0;
for(int i = 0; i < num; i++) {
//由於從start開始的,因此不再需要再第start個頂點進行處理
if(start == i) continue;
int j = 0;
int k = 0;
int min = INF;
//在未被加入到最小生成樹的頂點中,找出權值最小的頂點
while(j < num) {
//若weights[j]=0,意味着"第j個節點已經被排序過"(或者說已經加入了最小生成樹中)
if(weights[j] != 0 && weights[j] < min) {
min = weights[j];
k = j;
}
j++;
}
// 經過上面的處理後,在未被加入到最小生成樹的頂點中,權值最小的頂點是第k個頂點。
// 將第k個頂點加入到最小生成樹的結果數組中
prims[index++] = vertex[k];
//將"第k個頂點的權值"標記爲0,意味着第k個頂點已經排序過了(或者說已經加入了最小樹結果中)
weights[k] = 0;
// 當第k個頂點被加入到最小生成樹的結果數組中之後,更新其它頂點的權值。
for(j = 0; j < num; j++) {
//當第j個節點沒有被處理,並且需要更新的時才更新
if(weights[j] != 0 && matrix[k][j] < weights[j])
weights[j] = matrix[k][j];
}
}
//計算最小生成樹的權值
int sum = 0;
for(int i = 1; i < index; i++) {
int min = INF;
//獲取prims[i]在matrix中的位置
int n = getLocation(prims[i]);
// 在vertex[0...i]中,找出到j的權值最小的頂點
for(int j = 0; j < i; j++) {
int m = getLocation(prims[j]);
if(matrix[m][n] < min) {
min = matrix[m][n];
}
}
sum +=min;
}
//打印最小生成樹
System.out.println("PRIM算法從" + vertex[start] + "點開始:");
System.out.println("路徑總權值爲:" + sum);
for(int i = 0; i < index; i++)
System.out.println("第" + (i + 1) + "次選擇:" + prims[i] + "點");
System.out.println();
}
}
克魯斯卡爾算法介紹:
Kruskal算法是一種用來查找最小生成樹的算法,由Joseph Kruskal在1956年發表。用來解決同樣問題的還有Prim算法和Boruvka算法等。三種算法都是貪心算法的應用。和Boruvka算法不同的地方是,Kruskal算法在圖中存在相同權值的邊時也有效。
算法思想:
- 先構造一個只含 n 個頂點、而邊集爲空的子圖,把子圖中各個頂點看成各棵樹上的根結點,
- 之後,從網的邊集 E 中選取一條權值最小的邊,若該條邊的兩個頂點分屬不同的樹,則將其加入子圖,即把兩棵樹合成一棵樹,反之,若該條邊的兩個頂點已落在同一棵樹上,則不可取,而應該取下一條權值最小的邊再試之。
- 依次類推,直到森林中只有一棵樹,也即子圖中含有 n-1 條邊爲止。
算法實現思路:
- 前面的算法思路和普里姆算法一樣。
- 區別在於:我們需要倆個結構體數組(EData:result/edges),保存Kruskal最小生成樹的邊,還有圖對應的邊。
- 我們還需要一個數組(endData),保存"已有最小生成樹"中每個頂點在該最小樹中的終點。
- 獲取圖的所有邊信息,賦給結構體數組。
- 按邊的權值進行排序。
- 開始循環的選取剛剛排好序列的邊結構體數組,如果添加此邊,沒有構成環,那麼把此邊加入到結果集,更改endData在"已有的最小生成樹"中的終點。
邊的結構體信息(單獨寫一個結構體類java文件):
public class EData {
char start; //邊的起點
char end; //邊的終點
int weight; //邊的權重
public EData(char start, char end, int weight) {
this.start = start;
this.end = end;
this.weight = weight;
}
}
Kruskal算法代碼:
/**
* 克魯斯卡爾(Kruskal)最小生成樹
* @param args
*/
public void Kruskal() {
int index = 0; //result數組的索引
int[] endData = new int[matrixNum]; //用於保存"已有最小生成樹"中每個頂點在該最小樹中的終點
EData[] result = new EData[matrixNum];//結果數組,保存kruskal最小生成樹的邊
EData[] edges; //圖對應的所有邊
//獲取"圖中所有的邊"
edges = getEdges();
//將邊按"權"的大小進行排序(從小到大)
sortEdges(edges, matrixNum);
for(int i = 0; i < matrixNum; i++) {
int start = getLocation(edges[i].start);//獲取第i條邊的"起點"的序號
int end = getLocation(edges[i].end); //獲取第i條邊的"終點"的序號
int m = getEnd(endData, start); //獲取start在"已有的最小生成樹"中的終點
int n = getEnd(endData, end); //獲取end在"已有的最小生成樹"中的終點
//如果m != n,意味着"邊i"與"已經添加到最小生成樹中的頂點"沒有形成環路
if(m != n) {
endData[m] = n; //設置m在"已有的最小生成樹"中的終點爲n
result[index++] = edges[i]; //保存結果
}
}
//統計並打印"kruskal最小生成樹"的信息
int length = 0;
for(int i = 0; i < index; i++)
length += result[i].weight;
System.out.println("Kruskal算法:");
System.out.println("路徑總權值爲:" + length);
for(int i = 0; i < index; i++)
System.out.println("第"+ (i + 1) + "次選:" + result[i].start + result[i].end + "邊");
System.out.println();
}
/**
* 對邊按照權值大小進行排序(從小到大)
*/
private void sortEdges(EData[] edges, int len) {
for(int i = 0; i < len; i++) {
for(int j = i + 1; j < len; j++) {
if(edges[i].weight > edges[j].weight) {
//交換 邊 i 和 邊 j
EData temp = edges[i];
edges[i] = edges[j];
edges[j] = temp;
}
}
}
}
/**
* 獲取i的終點
*/
private int getEnd(int[] endData, int i) {
while(endData[i] != 0)
i = endData[i];
return i;
}
/**
* 獲取圖中的邊
* @param args
*/
private EData[] getEdges() {
int index = 0;
EData[] edges;
edges = new EData[matrixNum];
for(int i = 0; i < vertex.length; i++) {
for(int j = i + 1; j < vertex.length; j++) {
if(matrix[i][j] != INF) {
edges[index++] = new EData(vertex[i], vertex[j], matrix[i][j]);
}
}
}
return edges;
}
}
main方法:
public static void main(String[] args) {
Graph ga = new Graph();
ga.prim(0); //普里姆算法 (從接收的第一個頂點開始)
ga.Kruskal(); //Krustal算法
}
測試數據:
7 11
A B C D E F G
A B 7
A D 5
D B 9
B C 8
B E 7
D E 15
D F 6
F E 8
F G 11
E G 9
C E 5
運行結果:
提示:
普里姆算法和Kruskal算法的各自代碼寫到一個類裏就可以直接運行。(結構體,單獨一個文件)