注意:
最小生成樹和最短路徑不一樣的,實際應用中就是:最小生成樹求的是經過所有城市的最短的那條路,最短路徑只是求兩個城市之間最短的那條路,它並不經過所有城市!
算圖的最小生成樹有兩種算法,一種是普利姆算法,還有一種是克魯斯卡爾算法,普利姆算法的好處是可以指定起點,克魯斯卡爾算法的好處是它找出來的最小生成樹肯定是那個最小的最小生成樹;
申明:圖是偷的這個博主的 → 最小生成樹之java實現
一、普利姆算法
算法思想:
- 取圖中任意一個頂點v作爲生成樹的根
- 選擇一個頂點在生成樹中,另一個頂點不在生成樹中的邊權最小的邊,將頂點以及邊添加至生成樹中
- 繼續執行步驟2,直至生成樹上含有n-1條邊爲止
1、無權圖的最小生成樹
無權的實現很簡單:
隨便從一個頂點開始找一個經過所有節點的路徑即可;
下面的代碼也是基於深度優先搜索實現的:
/**
* @ClassName Node
* @Description 圖節點
* @Author lzq
* @Date 2019/6/19 04:39
* @Version 1.0
**/
public class Node {
public char label; //存放的數據
public boolean wasVisited; //記錄有無被訪問過
public Node(char label) {
this.label = label;
this.wasVisited = false;
}
}
import java.util.Stack;
/**
* @ClassName Graph2
* @Description 圖——最小生成樹
* @Author lzq
* @Date 2019/6/19 06:01
* @Version 1.0
**/
public class Graph {
private final int MAX_VERTS = 20; //表示一個圖節點能連接的最大定點數
private Node[] nodeList; //頂點數組
private int[][] adjMal; //鄰接矩陣,用來存方節點之間關係的
private int nNode; //當前頂點數量
private Stack<Integer> stack; //深度優先遍歷需要用到
public Graph() {
nodeList = new Node[MAX_VERTS];
adjMal = new int[MAX_VERTS][MAX_VERTS];
nNode = 0;
for (int i = 0; i < MAX_VERTS; i++) {
for (int j = 0; j < MAX_VERTS; j++) {
adjMal[i][j] = 0;
}
}
stack = new Stack<>();
}
/**
* 添加節點
* @param lab
*/
public void addNode(char lab) {
nodeList[nNode++] = new Node(lab);
}
/**
* 添加邊
* @param start
* @param end
*/
public void addEdge(int start,int end) {
adjMal[start][end] = 1;
adjMal[end][start] = 1;
}
/**
* 打印
* @param v
*/
public void displayNode(int v) {
System.out.print(nodeList[v].label);
}
/**
* 最小生成樹
*/
public void mxt() {
//默認從頂點中的第一個節點開始
nodeList[0].wasVisited = true;
stack.push(0);
while (!stack.empty()) {
int c = stack.peek();
int v = getAdjUnvisiteNode(c);
if(v == -1) { //沒有找到鄰接的沒有訪問的節點
stack.pop();
}else {
nodeList[v].wasVisited = true;
stack.push(v);
displayNode(c); //起點
System.out.print("-->");
displayNode(v); //終點
System.out.print("\t");
}
}
//到這所有的節點都訪問玩了,需要把訪問狀態改回去
for (int i = 0; i < nNode; i++) {
nodeList[i].wasVisited = false;
}
}
/**
* 找到指定節點鄰接的未被訪問的節點
* @param v
* @return
*/
private int getAdjUnvisiteNode(int v) {
for (int i = 0; i < nNode; i++) {
//代表兩個頂點之間是聯通的,並且這個頂點沒有被訪問過
if(adjMal[v][i] == 1 && !nodeList[i].wasVisited) {
return i;
}
}
return -1;
}
}
測試代碼:
public static void main(String[] args) {
Graph graph = new Graph();
graph.addNode('A');
graph.addNode('B');
graph.addNode('C');
graph.addNode('D');
graph.addNode('E');
graph.addEdge(0,1);
graph.addEdge(0,3);
graph.addEdge(1,2);
graph.addEdge(1,3);
graph.addEdge(1,4);
graph.addEdge(2,3);
graph.addEdge(2,4);
graph.addEdge(3,4);
graph.mxt();
}
優先結果:
如果拿一個方法寫的話就是這樣:
/**
* 求無權圖的最小生成樹
* @param nums 表示各節點之間是否有連線
* 1表示有 -1表示沒有或者其他的表示方法
* 只需要在getToIndex裏面改就是了
* @param startIndex 指定起始節點
* @return
*/
public static void getMinTree(int[][] nums,int startIndex) {
//記錄那些節點被遍歷過了
boolean[] sign = new boolean[nums.length];
//儲存臨時起始節點的
Stack<Integer> stack = new Stack<>();
stack.push(startIndex);
sign[startIndex] = true;
while (!stack.empty()) {
int fromIndex = stack.peek(); //當前起點
int toIndex = getToIndex(nums,sign,fromIndex); //下一個點
if(toIndex == -1) {
stack.pop(); //沒找到的話直接出棧
}else {
System.out.print(fromIndex+"-->"+toIndex+"\t"); //打印
sign[toIndex] = true; //標記
stack.push(toIndex); //這個節點就是下一次的新起點
}
}
}
/**
* 找一個與當前起點相鄰的未被訪問的、能到達的節點
* @param nums
* @param sign
* @param fromIndex
* @return
*/
private static int getToIndex(int[][] nums, boolean[] sign, int fromIndex) {
for (int i = 0; i < nums.length; i++) {
if(i == fromIndex) {
continue; //跳過自己
}
//沒有被訪問、並且和起點之間有連線的
if(!sign[i] && nums[i][fromIndex] == 1) {
return i;
}
}
return -1; //沒有了
}
測試代碼和運行結果:
public static void main(String[] args) {
int[][] nums = {{0,1,0,1,0},
{1,0,1,1,1},
{0,1,0,1,1},
{1,1,1,0,1},
{0,1,1,1,0},};
getMinTree(nums,0);
}
2、帶權圖的最小生成樹
帶權圖的最小生成樹麻煩點,它的最小生成樹算法過程:從一個頂點X(源點)出發找到其他頂點的所有邊,放入優先隊列,找到權值最小的,把它和所到達的頂點(終點Y)放入樹的集合中,再以終點Y作爲源點找到所有到其他頂點的邊(不包括已放入樹中的頂點),放到優先隊列中,再從中取最小的把它和它所到達的頂點(終點)放入樹的集合中,反覆這樣操作到全部頂點都放入樹中爲止;
1、無向帶權圖的最小生成樹
它的最小生成樹:
例如上訴圖例以A爲起點找最小生成樹的過程就是:
聲明:不能把在優先隊列裏面已經存在的邊、或已經走過的邊加到優先隊列裏面去;
- 以A爲起點,將A標記,將A能到達的邊(AB、AD)裝入優先隊列,找最小的邊即AB,刪除優先隊列裏面AB這條邊;此時A被標記,優先隊列裏面(AD);
- 以B爲起點,將B標記,將B能到達的邊裝入優先隊列,此時優先隊列裏面的邊有(AD、BD、BC、BE),找到最小的邊BE,刪除優先隊列裏面的BE這條邊;此時A、B被標記,優先隊列裏面剩下(AD、BD、BC);
- 以E爲起點,將E標記,將E能到達的邊裝入優先隊列,此時優先隊列裏面的邊有(AD、BD、BC、EC、ED),找到最小的邊CE,刪除優先隊列裏面的CE這條邊;此時A、B、E被標記,優先隊列裏面剩下(AD、BD、BC、ED);
- 以C爲起點,將C標記,將C能到達的邊裝入優先隊列,此時優先隊列裏面的邊有(AD、BD、BC、ED、CD),找到最小的邊CD,刪除優先隊列裏面的CD這條邊;此時A、B、E、C被標記,優先隊列裏面剩下(AD、BD、BC、ED);
- 以D爲起點,將D標記,此時A、B、E、C、D全部被標記,最小生成樹查找完成;
下面是代嗎實現(把上面的代碼中getToIndex方法改一下,有權圖需要在所有已經放到優先隊列裏面的邊裏面選最短的):
/**
* 求有權圖的最小生成樹
* @param nums 表示各節點之間是否有連線
* -1表示不能到達、0表示到自己
*
* @param startIndex 指定起始節點
* @return
*/
public static void getMinTree(int[][] nums,int startIndex) {
boolean[] sign = new boolean[nums.length];
//儲存臨時起始節點的
Stack<Integer> stack = new Stack<>();
//棧裏面裝的是前幾次選的起始節點,後面找最近距離的時候需要翻翻前面的
stack.push(startIndex);
sign[startIndex] = true;
while (!stack.empty()) {
int[] index = getToIndex(nums,sign,(Stack<Integer>) stack.clone());
int formIndex = index[0];
int toIndex = index[1];
if(toIndex == -1) {
stack.pop();
}else {
sign[toIndex] = true;
System.out.print(formIndex+"-->"+toIndex+"\t");
stack.push(toIndex);
}
}
}
/**
* 相當於從優先隊列裏面找最短的那個,並返回最短距離對應的起始、終止節點
* @param nums
* @param sign
* @param stack
* @return
*/
private static int[] getToIndex(int[][] nums, boolean[] sign,Stack<Integer> stack) {
int formIndex = stack.peek(); //記錄起點
int maxVlaue = Integer.MAX_VALUE;
int index = -1; //記錄對應的終點
//在前面所有選過的節點對應的邊裏面挑最短的
while (!stack.empty()) {
int tempStart = stack.pop(); //取出一個作爲臨時起點
for (int i = 0; i < nums.length; i++) {
if(tempStart == i) {
continue; //跳過自己
}
//沒有被訪問、並且和起點之間有連線的、且連線是最短的
if(!sign[i] && nums[tempStart][i] != -1 && nums[tempStart][i] < maxVlaue) {
formIndex = tempStart;
maxVlaue = nums[tempStart][i];
index = i;
}
}
}
return new int[] {formIndex,index}; //返回結果
}
上述圖例的運行結果:
測試代碼:
public static void main(String[] args) {
int[][] nums = {{0,5,-1,8,-1},
{5,0,6,9,5},
{-1,6,0,2,4},
{8,9,2,0,7},
{-1,5,4,7,0},};
getMinTree(nums,0);
}
2、有向帶權圖的最小生成樹
假設還是以A爲起點,那麼它的最小生成樹是:
這個和無向帶權圖的方法差不多,需要改變的就是數組的輸入,還有就是兩點之間有一個方向上能通就可以,注意我註釋注意的地方:
以下就是更改後的有向帶權圖的最小生成樹的代碼:
/**
* 求有權圖的最小生成樹
* @param nums 表示各節點之間是否有連線
* -1表示不能到達、0表示到自己
*
* @param startIndex 指定起始節點
* @return
*/
public static void getMinTree(int[][] nums,int startIndex) {
boolean[] sign = new boolean[nums.length];
//儲存臨時起始節點的
Stack<Integer> stack = new Stack<>();
//棧裏面裝的是前幾次選的起始節點,後面找最近距離的時候需要翻翻前面的
stack.push(startIndex);
sign[startIndex] = true;
while (!stack.empty()) {
int[] index = getToIndex(nums,sign,(Stack<Integer>) stack.clone());
int formIndex = index[0];
int toIndex = index[1];
if(toIndex == -1) {
stack.pop();
}else {
// ============注意====================
if(sign[formIndex]) {
sign[toIndex] = true;
stack.push(toIndex);
}else {
sign[formIndex] = true;
stack.push(formIndex);
}
System.out.print(formIndex+"-->"+toIndex+"\t");
}
}
}
/**
* 相當於從優先隊列裏面找最短的那個,並返回最短距離對應的起始、終止節點
* @param nums
* @param sign
* @param stack
* @return
*/
private static int[] getToIndex(int[][] nums, boolean[] sign,Stack<Integer> stack) {
int formIndex = stack.peek(); //記錄起點
int maxVlaue = Integer.MAX_VALUE;
int index = -1; //記錄對應的終點
//在前面所有選過的節點對應的邊裏面挑最短的
while (!stack.empty()) {
int tempStart = stack.pop(); //取出一個作爲臨時起點
for (int i = 0; i < nums.length; i++) {
if(tempStart == i) {
continue; //跳過自己
}
// ==================注意=======================
//沒有被訪問、並且和起點之間有連線的(兩個方向能通就可以)、且連線是最短的
if(!sign[i]) {
if(nums[tempStart][i] != -1 && nums[tempStart][i] < maxVlaue) {
formIndex = tempStart;
maxVlaue = nums[tempStart][i];
index = i;
}
if(nums[i][tempStart] != -1 && nums[i][tempStart] < maxVlaue) {
formIndex = i;
maxVlaue = nums[i][tempStart];
index = tempStart;
}
}
}
}
return new int[] {formIndex,index}; //返回結果
}
然後更改一下數組的輸入,我們寫測試代碼吧:
public static void main(String[] args) {
int[][] nums = {{0,5,-1,8,-1},
{-1,0,6,9,-1},
{-1,-1,0,-1,4},
{-1,-1,2,0,7},
{-1,5,-1,-1,0},};
getMinTree(nums,0);
}
運行結果:
二、克魯斯卡爾
求最小生成樹有兩種算法,一種是普利姆算法,我用的就是,還有一種是克魯斯卡爾算法,有興趣的可以自己寫一下,這是資料鏈接 → 最小生成樹之java實現
最小生成樹的起點不一樣的話,一般不會影響最小生成樹的結果,(如果所有邊的權值都不相等的話,以任何不同的節點爲起始節點,他們的最小生成樹肯定是一樣的,如果有那麼幾條邊的權值相等,這就可能會造成最小生成樹路徑不同,但路徑和肯定是一樣的);
我們先拿這個圖(無向有權圖)來試一下吧:
測試代碼:
接着再來試一下有向有權圖的:
public static void main(String[] args) {
int[][] nums = {{0,5,-1,8,-1},
{5,0,6,9,5},
{-1,6,0,2,4},
{8,9,2,0,7},
{-1,5,4,7,0},};
for (int i = 0; i < nums.length; i++) {
getMinTree(nums,i);
System.out.println();
}
}
可以看到,除過方向和位置的改變(因爲無向圖本就不涉及方向),選擇的邊幾乎沒變,我們再來試試有向圖的吧:
測試代碼:
public static void main(String[] args) {
int[][] nums = {{0,5,-1,8,-1},
{-1,0,6,9,-1},
{-1,-1,0,-1,4},
{-1,-1,2,0,7},
{-1,5,-1,-1,0},};
for (int i = 0; i < nums.length; i++) {
getMinTree(nums,i);
System.out.println();
}
}
運行結果:
可以看到,基本變得也就是順序,下面採用克魯斯卡爾算法,結果都是一樣的,因爲它會自己從最短的那條邊開始找最小生成樹:
算法思想:
- 將圖中全部頂點放入生成樹中
- 選擇聯結不同連通分量邊權最小的邊,將邊添加至生成樹當中
- 繼續執行步驟2,直至生成樹上含有n-1條邊爲止
注:如果圖中兩個頂點之間存在拓展的邊則稱這兩個頂點爲同一連通分量。
代碼:
public static void getMinTree(int[][] nums) {
nums = nums.clone();
int i = 1;
while (i < nums.length) { //找n-1次就找完了
int[] path = getToIndex(nums);
int fromIndex = path[0]; //起點
int toIndex = path[1]; //終點
nums[fromIndex][toIndex] = -2; //表示走過了
i++;
System.out.print(fromIndex+"-->"+toIndex+"\t");
}
}
private static int[] getToIndex(int[][] nums) {
int maxVlaue = Integer.MAX_VALUE; //最小距離
int fromIndex = -1; //起點
int toIndex = -1; //終點
for (int i = 0; i < nums.length; i++) {
for (int j = 0; j < nums[i].length; j++) {
//如果是自己到自己、或者兩點之間無法到達、或者走過了直接跳過
if(i == j || nums[i][j] <= -1) {
continue;
}
if(nums[i][j] < maxVlaue) {
maxVlaue = nums[i][j];
fromIndex = i;
toIndex = j;
}
}
}
return new int[] {fromIndex,toIndex};
}
測試:
public static void main(String[] args) {
int[][] nums = {{0, 5, -1, 8, -1},
{-1, 0, 6, 9, -1},
{-1, -1, 0, -1, 4},
{-1, -1, 2, 0, 7},
{-1, 5, -1, -1, 0},};
getMinTree(nums);
}