最小生成樹
在含有n個頂點的連通圖中選擇n-1條邊,構成一棵極小連通子圖,并使該連通子圖中n-1條邊上權值之和達到最小,則稱其為連通網的最小生成樹。
例如,對于上圖中的連通網可以有多棵權值總和不相同的生成樹。
Kruskal算法
1.介紹
克魯斯卡爾(Kruskal)算法,是用來求加權連通圖的最小生成樹的算法。
基本思想:按照權值從小到大的順序選擇n-1條邊,并保證這n-1條邊不構成回路。
具體做法:首先構造一個只含n個頂點的森林,然后依照權值從小到大從連通網中選擇邊加入到森林中,并使得森林不產生回路,直到森林變成一棵樹為止。
2.圖解
以圖G4為例(更詳細的可以參考《算法導論》p367),對Kruskal進行演示(假設,用數組R保存最小生成樹結果)。
第1步:將邊<E,F>加入R中。
邊<E,F>的權值最小,因此將它加入到最小生成樹結果R中。
第2步:將邊<C,D>加入R中。
上一步操作之后,邊<C,D>的權值最小,因此將它加入到最小生成樹結果R中。
第3步:將邊<D,E>加入R中。
上一步操作之后,邊<D,E>的權值最小,因此將它加入到最小生成樹結果R中。
第4步:將邊<B,F>加入R中。
上一步操作之后,邊<C,E>的權值最小,但<C,E>會和已有的邊構成回路;因此,跳過邊<C,E>。同理,跳過邊<C,F>。將邊<B,F>加入到最小生成樹結果R中。
第5步:將邊<E,G>加入R中。
上一步操作之后,邊<E,G>的權值最小,因此將它加入到最小生成樹結果R中。
第6步:將邊<A,B>加入R中。
上一步操作之后,邊<F,G>的權值最小,但<F,G>會和已有的邊構成回路;因此,跳過邊<F,G>。同理,跳過邊<B,C>。將邊<A,B>加入到最小生成樹結果R中。
此時,最小生成樹構造完成!它包括的邊依次是:<E,F> <C,D> <D,E> <B,F> <E,G> <A,B>。
3.算法分析
根據前面介紹的克魯斯卡爾算法的基本思想和做法,我們能夠了解到,克魯斯卡爾算法重點需要解決的以下兩個問題:
問題一 對圖的所有邊按照權值大小進行排序。
問題二 將邊添加到最小生成樹中時,怎么樣判斷是否形成了回路。
問題一用排序算法排序即可。
問題二,處理方式:記錄頂點在“最小生成樹”中的終點,頂點的終點是“在最小生成樹中與它連通的最大頂點"(關于這一點,后面會通過圖片給出說明)。然后每次需要將一條邊添加到最小生成樹時,判斷該邊的兩個頂點的終點是否重合,重合的話則會構成回路。 以下圖來進行說明:
在將<E,F> <C,D> <D,E>加入到最小生成樹R中之后,這幾條邊的頂點就都有了終點:
- (01) C的終點是F。
- (02) D的終點是F。
- (03) E的終點是F。
- (04) F的終點是F。
關于終點,就是將所有頂點按照從小到大的順序排列好之后;某個頂點的終點就是"與它連通的最大頂點"。 因此,接下來,雖然<C,E>是權值最小的邊。但是C和E的重點都是F,即它們的終點相同,因此,將<C,E>加入最小生成樹的話,會形成回路。這就是判斷回路的方式。
4.代碼實現
package com.xushu;
/**
* Java Kruskal算法生成最小生成樹(鄰接表)
*/
import java.io.IOException;
import java.util.Scanner;
public class ListUDG {
private static int INF = Integer.MAX_VALUE;
// 鄰接表中表對應的鏈表的頂點
private class ENode {
int ivex; // 該邊所指向的頂點的位置
int weight; // 該邊的權
ENode nextEdge; // 指向下一條弧的指針
}
// 鄰接表中表的頂點
private class VNode {
char data; // 頂點信息
ENode firstEdge; // 指向第一條依附該頂點的弧
};
private int mEdgNum; // 邊的數量
private VNode[] mVexs; // 頂點數組
// /*
// * 創建圖(自己輸入數據)
// */
// public ListUDG() {
//
// // 輸入"頂點數"和"邊數"
// System.out.printf("input vertex number: ");
// int vlen = readInt();
// System.out.printf("input edge number: ");
// int elen = readInt();
// if ( vlen < 1 || elen < 1 || (elen > (vlen*(vlen - 1)))) {
// System.out.printf("input error: invalid parameters!\n");
// return ;
// }
//
// // 初始化"頂點"
// mVexs = new VNode[vlen];
// for (int i = 0; i < mVexs.length; i++) {
// System.out.printf("vertex(%d): ", i);
// mVexs[i] = new VNode();
// mVexs[i].data = readChar();
// mVexs[i].firstEdge = null;
// }
//
// // 初始化"邊"
// mEdgNum = elen;
// for (int i = 0; i < elen; i++) {
// // 讀取邊的起始頂點和結束頂點
// System.out.printf("edge(%d):", i);
// char c1 = readChar();
// char c2 = readChar();
// int weight = readInt();
//
// int p1 = getPosition(c1);
// int p2 = getPosition(c2);
// // 初始化node1
// ENode node1 = new ENode();
// node1.ivex = p2;
// node1.weight = weight;
// // 將node1鏈接到"p1所在鏈表的末尾"
// if(mVexs[p1].firstEdge == null)
// mVexs[p1].firstEdge = node1;
// else
// linkLast(mVexs[p1].firstEdge, node1);
// // 初始化node2
// ENode node2 = new ENode();
// node2.ivex = p1;
// node2.weight = weight;
// // 將node2鏈接到"p2所在鏈表的末尾"
// if(mVexs[p2].firstEdge == null)
// mVexs[p2].firstEdge = node2;
// else
// linkLast(mVexs[p2].firstEdge, node2);
// }
// }
/*
* 創建圖(用已提供的矩陣)
*
* 參數說明:
* vexs -- 頂點數組
* edges -- 邊
*/
public ListUDG(char[] vexs, EData[] edges) {
// 初始化"頂點數"和"邊數"
int vlen = vexs.length;
int elen = edges.length;
// 初始化"頂點"
mVexs = new VNode[vlen];
for (int i = 0; i < mVexs.length; i++) {
mVexs[i] = new VNode();
mVexs[i].data = vexs[i];
mVexs[i].firstEdge = null;
}
// 初始化"邊"
mEdgNum = elen;
for (int i = 0; i < elen; i++) {
// 讀取邊的起始頂點和結束頂點
char c1 = edges[i].start;
char c2 = edges[i].end;
int weight = edges[i].weight;
// 讀取邊的起始頂點和結束頂點
int p1 = getPosition(c1);
int p2 = getPosition(c2);
// 初始化node1
ENode node1 = new ENode();
node1.ivex = p2;
node1.weight = weight;
// 將node1鏈接到"p1所在鏈表的末尾"
if(mVexs[p1].firstEdge == null)
mVexs[p1].firstEdge = node1;
else
linkLast(mVexs[p1].firstEdge, node1);
// 初始化node2
ENode node2 = new ENode();
node2.ivex = p1;
node2.weight = weight;
// 將node2鏈接到"p2所在鏈表的末尾"
if(mVexs[p2].firstEdge == null)
mVexs[p2].firstEdge = node2;
else
linkLast(mVexs[p2].firstEdge, node2);
}
}
/*
* 將node節點鏈接到list的最后
*/
private void linkLast(ENode list, ENode node) {
ENode p = list;
while(p.nextEdge!=null)
p = p.nextEdge;
p.nextEdge = node;
}
/*
* 返回ch位置
*/
private int getPosition(char ch) {
for(int i=0; i<mVexs.length; i++)
if(mVexs[i].data==ch)
return i;
return -1;
}
/*
* 讀取一個輸入字符
*/
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;
}
/*
* 讀取一個輸入字符
*/
private int readInt() {
Scanner scanner = new Scanner(System.in);
return scanner.nextInt();
}
/*
* 打印矩陣隊列圖
*/
public void print() {
System.out.printf("List Graph:\n");
for (int i = 0; i < mVexs.length; i++) {
System.out.printf("%d(%c): ", i, mVexs[i].data);
ENode node = mVexs[i].firstEdge;
while (node != null) {
System.out.printf("%d(%c) ", node.ivex, mVexs[node.ivex].data);
node = node.nextEdge;
}
System.out.printf("\n");
}
}
/*
* 獲取邊<start, end>的權值;若start和end不是連通的,則返回無窮大。
*/
private int getWeight(int start, int end) {
if (start==end)
return 0;
ENode node = mVexs[start].firstEdge;
while (node!=null) {
if (end==node.ivex)
return node.weight;
node = node.nextEdge;
}
return INF;
}
/*
* 克魯斯卡爾(Kruskal)最小生成樹
*/
public void kruskal() {
int index = 0; // rets數組的索引
int[] vends = new int[mEdgNum]; // 用于保存"已有最小生成樹"中每個頂點在該最小樹中的終點。
EData[] rets = new EData[mEdgNum]; // 結果數組,保存kruskal最小生成樹的邊
EData[] edges; // 圖對應的所有邊
// 獲取"圖中所有的邊"
edges = getEdges();
// 將邊按照"權"的大小進行排序(從小到大)
sortEdges(edges, mEdgNum);
for (int i=0; i<mEdgNum; i++) {
int p1 = getPosition(edges[i].start); // 獲取第i條邊的"起點"的序號
int p2 = getPosition(edges[i].end); // 獲取第i條邊的"終點"的序號
int m = getEnd(vends, p1); // 獲取p1在"已有的最小生成樹"中的終點
int n = getEnd(vends, p2); // 獲取p2在"已有的最小生成樹"中的終點
// 如果m!=n,意味著"邊i"與"已經添加到最小生成樹中的頂點"沒有形成環路
if (m != n) {
vends[m] = n; // 設置m在"已有的最小生成樹"中的終點為n
rets[index++] = edges[i]; // 保存結果
}
}
// 統計并打印"kruskal最小生成樹"的信息
int length = 0;
for (int i = 0; i < index; i++)
length += rets[i].weight;
System.out.printf("Kruskal=%d: ", length);
for (int i = 0; i < index; i++)
System.out.printf("(%c,%c) ", rets[i].start, rets[i].end);
System.out.printf("\n");
}
/*
* 獲取圖中的邊
*/
private EData[] getEdges() {
int index=0;
EData[] edges;
edges = new EData[mEdgNum];
for (int i=0; i < mVexs.length; i++) {
ENode node = mVexs[i].firstEdge;
while (node != null) {
if (node.ivex > i) {
edges[index++] = new EData(mVexs[i].data, mVexs[node.ivex].data, node.weight);
}
node = node.nextEdge;
}
}
return edges;
}
/*
* 對邊按照權值大小進行排序(由小到大)
*/
private void sortEdges(EData[] edges, int elen) {
for (int i=0; i<elen; i++) {
for (int j=i+1; j<elen; j++) {
if (edges[i].weight > edges[j].weight) {
// 交換"邊i"和"邊j"
EData tmp = edges[i];
edges[i] = edges[j];
edges[j] = tmp;
}
}
}
}
/*
* 獲取i的終點
*/
private int getEnd(int[] vends, int i) {
while (vends[i] != 0)
i = vends[i];
return i;
}
// 邊的結構體
private static 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;
}
};
public static void main(String[] args) {
char[] vexs = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
EData[] edges = {
// 起點 終點 權
new EData('A', 'B', 12),
new EData('A', 'F', 16),
new EData('A', 'G', 14),
new EData('B', 'C', 10),
new EData('B', 'F', 7),
new EData('C', 'D', 3),
new EData('C', 'E', 5),
new EData('C', 'F', 6),
new EData('D', 'E', 4),
new EData('E', 'F', 2),
new EData('E', 'G', 8),
new EData('F', 'G', 9),
};
ListUDG pG;
// 自定義"圖"(輸入矩陣隊列)
//pG = new ListUDG();
// 采用已有的"圖"
pG = new ListUDG(vexs, edges);
pG.print(); // 打印圖
pG.kruskal(); // Kruskal算法生成最小生成樹
}
}
Prim算法
1.介紹
普里姆(Prim)算法,也是求加權連通圖的最小生成樹的算法。
基本思想
對于圖G而言,V是所有頂點的集合;現在,設置兩個新的集合U和T,其中U用于存放G的最小生成樹中的頂點,T存放G的最小生成樹中的邊。從所有的 u?U ,v?(V-U)(V-U表示除去U的所有頂點)的邊中選取權值最小的邊(u,v),將頂點v加入U中,將邊(u,v)加入集合T中,如此不斷重復,直到U=V為止,最小生成樹構造完畢,此時集合T中包含了最小生成樹中的所有邊。
2.圖解
以上圖G4為例,來對普里姆進行演示(從第一個頂點A開始通過普里姆算法生成最小生成樹)。
初始狀態:V是所有頂點的集合,即V={A,B,C,D,E,F,G};U和T都是空!
第1步:將頂點A加入到U中。
此時,U={A}。
第2步:將頂點B加入到U中。
上一步操作之后,U={A}, V-U={B,C,D,E,F,G};因此,邊(A,B)的權值最小。將頂點B添加到U中;此時,U={A,B}。
第3步:將頂點F加入到U中。
上一步操作之后,U={A,B}, V-U={C,D,E,F,G};因此,邊(B,F)的權值最小。將頂點F添加到U中;此時,U={A,B,F}。
第4步:將頂點E加入到U中。
上一步操作之后,U={A,B,F}, V-U={C,D,E,G};因此,邊(F,E)的權值最小。將頂點E添加到U中;此時,U={A,B,F,E}。
第5步:將頂點D加入到U中。
上一步操作之后,U={A,B,F,E}, V-U={C,D,G};因此,邊(E,D)的權值最小。將頂點D添加到U中;此時,U={A,B,F,E,D}。
第6步:將頂點C加入到U中。
上一步操作之后,U={A,B,F,E,D}, V-U={C,G};因此,邊(D,C)的權值最小。將頂點C添加到U中;此時,U={A,B,F,E,D,C}。
第7步:將頂點G加入到U中。
上一步操作之后,U={A,B,F,E,D,C}, V-U={G};因此,邊(F,G)的權值最小。將頂點G添加到U中;此時,U=V。
此時,最小生成樹構造完成!它包括的頂點依次是:A B F E D C G。
4.代碼實現
package com.xushu;
/**
* Java prim算法生成最小生成樹(鄰接表)
*/
import java.io.IOException;
import java.util.Scanner;
public class ListUDG {
private static int INF = Integer.MAX_VALUE;
// 鄰接表中表對應的鏈表的頂點
private class ENode {
int ivex; // 該邊所指向的頂點的位置
int weight; // 該邊的權
ENode nextEdge; // 指向下一條弧的指針
}
// 鄰接表中表的頂點
private class VNode {
char data; // 頂點信息
ENode firstEdge; // 指向第一條依附該頂點的弧
};
private VNode[] mVexs; // 頂點數組
/*
* 創建圖(用已提供的矩陣)
*
* 參數說明:
* vexs -- 頂點數組
* edges -- 邊
*/
public ListUDG(char[] vexs, EData[] edges) {
// 初始化"頂點數"和"邊數"
int vlen = vexs.length;
int elen = edges.length;
// 初始化"頂點"
mVexs = new VNode[vlen];
for (int i = 0; i < mVexs.length; i++) {
mVexs[i] = new VNode();
mVexs[i].data = vexs[i];
mVexs[i].firstEdge = null;
}
// 初始化"邊"
for (int i = 0; i < elen; i++) {
// 讀取邊的起始頂點和結束頂點
char c1 = edges[i].start;
char c2 = edges[i].end;
int weight = edges[i].weight;
// 讀取邊的起始頂點和結束頂點
int p1 = getPosition(c1);
int p2 = getPosition(c2);
// 初始化node1
ENode node1 = new ENode();
node1.ivex = p2;
node1.weight = weight;
// 將node1鏈接到"p1所在鏈表的末尾"
if(mVexs[p1].firstEdge == null)
mVexs[p1].firstEdge = node1;
else
linkLast(mVexs[p1].firstEdge, node1);
// 初始化node2
ENode node2 = new ENode();
node2.ivex = p1;
node2.weight = weight;
// 將node2鏈接到"p2所在鏈表的末尾"
if(mVexs[p2].firstEdge == null)
mVexs[p2].firstEdge = node2;
else
linkLast(mVexs[p2].firstEdge, node2);
}
}
/*
* 將node節點鏈接到list的最后
*/
private void linkLast(ENode list, ENode node) {
ENode p = list;
while(p.nextEdge!=null)
p = p.nextEdge;
p.nextEdge = node;
}
/*
* 返回ch位置
*/
private int getPosition(char ch) {
for(int i=0; i<mVexs.length; i++)
if(mVexs[i].data==ch)
return i;
return -1;
}
/*
* 讀取一個輸入字符
*/
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;
}
/*
* 讀取一個輸入字符
*/
private int readInt() {
Scanner scanner = new Scanner(System.in);
return scanner.nextInt();
}
/*
* 打印矩陣隊列圖
*/
public void print() {
System.out.printf("List Graph:\n");
for (int i = 0; i < mVexs.length; i++) {
System.out.printf("%d(%c): ", i, mVexs[i].data);
ENode node = mVexs[i].firstEdge;
while (node != null) {
System.out.printf("%d(%c) ", node.ivex, mVexs[node.ivex].data);
node = node.nextEdge;
}
System.out.printf("\n");
}
}
/*
* 獲取邊<start, end>的權值;若start和end不是連通的,則返回無窮大。
*/
private int getWeight(int start, int end) {
if (start==end)
return 0;
ENode node = mVexs[start].firstEdge;
while (node!=null) {
if (end==node.ivex)
return node.weight;
node = node.nextEdge;
}
return INF;
}
/*
* prim最小生成樹
*
* 參數說明:
* start -- 從圖中的第start個元素開始,生成最小樹
*/
public void prim(int start) {
int min,i,j,k,m,n,tmp,sum;
int num = mVexs.length;
int index=0; // prim最小樹的索引,即prims數組的索引
char[] prims = new char[num]; // prim最小樹的結果數組
int[] weights = new int[num]; // 頂點間邊的權值
// prim最小生成樹中第一個數是"圖中第start個頂點",因為是從start開始的。
prims[index++] = mVexs[start].data;
// 初始化"頂點的權值數組",
// 將每個頂點的權值初始化為"第start個頂點"到"該頂點"的權值。
for (i = 0; i < num; i++ )
weights[i] = getWeight(start, i);
for (i = 0; i < num; i++) {
// 由于從start開始的,因此不需要再對第start個頂點進行處理。
if(start == i)
continue;
j = 0;
k = 0;
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++] = mVexs[k].data;
// 將"第k個頂點的權值"標記為0,意味著第k個頂點已經排序過了(或者說已經加入了最小樹結果中)。
weights[k] = 0;
// 當第k個頂點被加入到最小生成樹的結果數組中之后,更新其它頂點的權值。
for (j = 0 ; j < num; j++) {
// 獲取第k個頂點到第j個頂點的權值
tmp = getWeight(k, j);
// 當第j個節點沒有被處理,并且需要更新時才被更新。
if (weights[j] != 0 && tmp < weights[j])
weights[j] = tmp;
}
}
// 計算最小生成樹的權值
sum = 0;
for (i = 1; i < index; i++) {
min = INF;
// 獲取prims[i]在矩陣表中的位置
n = getPosition(prims[i]);
// 在vexs[0...i]中,找出到j的權值最小的頂點。
for (j = 0; j < i; j++) {
m = getPosition(prims[j]);
tmp = getWeight(m, n);
if (tmp < min)
min = tmp;
}
sum += min;
}
// 打印最小生成樹
System.out.printf("PRIM(%c)=%d: ", mVexs[start].data, sum);
for (i = 0; i < index; i++)
System.out.printf("%c ", prims[i]);
System.out.printf("\n");
}
// 示例類:邊的結構體(用來演示)
private static 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;
}
};
public static void main(String[] args) {
char[] vexs = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
EData[] edges = {
// 起點 終點 權
new EData('A', 'B', 12),
new EData('A', 'F', 16),
new EData('A', 'G', 14),
new EData('B', 'C', 10),
new EData('B', 'F', 7),
new EData('C', 'D', 3),
new EData('C', 'E', 5),
new EData('C', 'F', 6),
new EData('D', 'E', 4),
new EData('E', 'F', 2),
new EData('E', 'G', 8),
new EData('F', 'G', 9),
};
ListUDG pG;
// 自定義"圖"(輸入矩陣隊列)
//pG = new ListUDG();
// 采用已有的"圖"
pG = new ListUDG(vexs, edges);
//pG.print(); // 打印圖
//pG.DFS(); // 深度優先遍歷
//pG.BFS(); // 廣度優先遍歷
pG.prim(0); // prim算法生成最小生成樹
}
}