Java實現普里姆和克魯斯卡爾——最小生成樹

普里姆算法介紹:
普里姆算法(Prim算法),圖論中的一種算法,可在加權連通圖裏搜索最小生成樹。意即由此算法搜索到的邊子集所構成的樹中,不但包括了連通圖裏的所有頂點。
圖解過程:
在這裏插入圖片描述
算法思想:

  1. 一個加權連通圖,其中頂點集合爲V,邊集合爲E;
  2. 初始化:Vnew = {x},其中x爲集合V中的任一節點(起始點),Enew = {},爲空;
  3. 重複下列操作,直到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算法在圖中存在相同權值的邊時也有效。
算法思想:

  1. 先構造一個只含 n 個頂點、而邊集爲空的子圖,把子圖中各個頂點看成各棵樹上的根結點,
  2. 之後,從網的邊集 E 中選取一條權值最小的邊,若該條邊的兩個頂點分屬不同的樹,則將其加入子圖,即把兩棵樹合成一棵樹,反之,若該條邊的兩個頂點已落在同一棵樹上,則不可取,而應該取下一條權值最小的邊再試之。
  3. 依次類推,直到森林中只有一棵樹,也即子圖中含有 n-1 條邊爲止。

算法實現思路:

  1. 前面的算法思路和普里姆算法一樣。
  2. 區別在於:我們需要倆個結構體數組(EData:result/edges),保存Kruskal最小生成樹的邊,還有圖對應的邊。
  3. 我們還需要一個數組(endData),保存"已有最小生成樹"中每個頂點在該最小樹中的終點。
  4. 獲取圖的所有邊信息,賦給結構體數組。
  5. 按邊的權值進行排序。
  6. 開始循環的選取剛剛排好序列的邊結構體數組,如果添加此邊,沒有構成環,那麼把此邊加入到結果集,更改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算法的各自代碼寫到一個類裏就可以直接運行。(結構體,單獨一個文件)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章