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算法的各自代码写到一个类里就可以直接运行。(结构体,单独一个文件)

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