最小生成树和普利姆算法及克鲁斯卡尔算法

2024-01-01 06:50

本文主要是介绍最小生成树和普利姆算法及克鲁斯卡尔算法,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、最小生成树的介绍

最小生成树(Minimum Cost Spanning Tree), 简称 MST。即给定一个带权的无向连通图,如何选取一棵生成树,使树上所有边上权的总和为最小,这叫最小生成树

在这里插入图片描述

  1. 给定一个带权的无向连通图,如何选取一棵生成树,使树上所有边上权的总和为最小,这叫最小生成树
  2. N个顶点,一定有N-1条边
  3. 包含全部顶点
  4. 求最小生成树的算法主要是普里姆算法和克鲁斯卡尔算法

二、普利姆算法

普利姆(Prim)算法求最小生成树,也就是在包含n个顶点的连通图中,找出只有(n-1)条边包含所有n个顶点的连通子图,也就是所谓的极小连通子图

2.1 普利姆算法的基本思路

  1. 设G=(V,E)是连通网,T=(U,D)是最小生成树,V,U是顶点集合,E,D是边的集合
  2. 若从顶点u开始构造最小生成树,则从集合V中取出顶点u放入集合U中,标记顶点v的visited[u]=1
  3. 若集合U中顶点ui与集合V-U中的顶点vj之间存在边,则寻找这些边中权值最小的边,但不能构成回路,将顶点vj加入集合U中,将边(ui,vj)加入集合D中,标记visited[vj]=1
  4. 重复步骤②,直到U与V相等,即所有顶点都被标记为访问过,此时D中有n-1条边

2.2 应用场景

有胜利乡有7个村庄(A, B, C, D, E, F, G),现在需要修路把7个村庄连通 各个村庄的距离用边线表示(权),比如 A – B 距离 5公里。问:如何修路保证各个村庄都能连通,并且总的修建公路总里程最短?
在这里插入图片描述

基本步骤:

  1. 首先,约定 V 为所有顶点的集合;约定 TE 为最小生成树中边的集合,初始为空;约定 U 为最小生成树中边的顶点集合,初始为空。

  2. 选择从 A 点开始普里姆算法,则 U = { A }。
    在这里插入图片描述

  3. 找出所有与集合 U 中的顶点相连的权重最小的边作为最小生成树的边。此时集合 U 中只有一个 A顶点,在集合 V-U 中与 A 顶点相连的边有 <A,B>[5]、<A,C>[7]、<A,G>[2]。其中权重最小的边为 <A,G>[2],因此我们选择其为最小生成树的一条边,然后将此边加入 TE 集合,将 G 顶点加入 U 集合。此时 TE = { <A,G> },U = { A,G }。
    在这里插入图片描述

  4. 继续找出所有与集合 U 中顶点相连的权重最小的边作为生成树的边。此时集合 U = { A、G },在顶点集合 V-U 中分别与顶点 A、C 相连的边有 <A、B>[5]、<A、C>[7]、<G、B>[3]、<G、E>[4]、<G、F>[6]。其中权重最小的边为 <G、B>[3],因此我们选取其为最小生成树的边,加入 TE 集合,将顶点 B 加入 U 集合。此时 TE = { <A,G> ,<G,B> },U = { A,G,B }。
    在这里插入图片描述

  5. 继续找出所有与集合 U 中顶点相连的权重最小的边作为生成树的边。此时集合 U = { A、G、B },在顶点集合 V-U 中分别与顶点 A、C 相连的边有 <A、C>[7]、<G、E>[4]、<G、F>[6]、<B、D>[9]。其中权重最小的边为 <G、E>[4],因此我们选取其为最小生成树的边,加入 TE 集合,将顶点 B 加入 U 集合。此时 TE = { <A,G> ,<G,B> ,<G、E>},U = { A,G,B,E}。
    在这里插入图片描述

  6. 继续找出所有与集合 U 中顶点相连的权重最小的边作为生成树的边。此时集合 U = { A、G、B、E },在顶点集合 V-U 中分别与顶点 A、C 相连的边有 <A、C>[7]、<E、C>[8]、<E、F>[5]、<G、F>[6]、<B、D>[9]。其中权重最小的边为 <E、F>[5],因此我们选取其为最小生成树的边,加入 TE 集合,将顶点 B 加入 U 集合。此时 TE = { <A,G> ,<G,B> ,<G、E>,<E、F>},U = { A,G,B,E,F}。
    在这里插入图片描述
    7.以此类推,最后得到的结果集为TE = { <A,G> ,<G,B> ,<G、E>,<E、F>,<F、D>,<A、C>},U = { A,G,B,E,F,D,C}。至此,集合 U 等于了 V,也就是构造出了最小生成树,最后将所有权值加起来的结果得到25,说明至少要修路25公里,普里姆算法结束。
    在这里插入图片描述

2.3 代码实现

package algorithm.prim;import java.util.Arrays;public class PrimAlgorithm {public static void main(String[] args) {char[] data = new char[]{'A','B','C','D','E','F','G'};int verxes = data.length;int[][] weight = {  // 邻接矩阵,约定 10000 代表不连通/*A*//*B*//*C*//*D*//*E*//*F*//*G*//*A*/{10000,5,7,10000,10000,10000,2},/*B*/{5,10000,10000,9,10000,10000,3},/*C*/{7,10000,10000,10000,8,10000,10000},/*D*/{10000,9,10000,10000,10000,4,10000},/*E*/{10000,10000,8,10000,10000,5,4},/*F*/{10000,10000,10000,4,5,10000,6},/*G*/{2,3,10000,10000,4,6,10000}};Graph graph = new Graph(verxes);MinTree minTree = new MinTree();minTree.createGraph(graph,verxes,data,weight);minTree.showGraph(graph);//普利姆算法minTree.prim(graph,0);}
}//创建最小生成树
class MinTree{//创建图的邻接矩阵/**** @param graph 图对象* @param verxes    图对应的顶点个数* @param data  图各个顶点的值* @param weight    图的邻接矩阵*/public void createGraph(Graph graph,int verxes,char data[],int[][] weight){int i,j;for(i = 0;i < verxes;i++){graph.data[i] = data[i];for(j = 0;j < verxes;j++){graph.weight[i][j] = weight[i][j];}}}//显示图的邻接矩阵public void showGraph(Graph graph){for(int[] link:graph.weight){System.out.println(Arrays.toString(link));}}//编写普利姆算法public void prim(Graph graph,int vNum){int[] visited = new int[graph.verxes];//判断顶点有没有被访问过,默认为0visited[vNum] = 1;//h1和h2记录两个顶点的下标int h1 = -1;int h2 = -2;int minWeight = 10000;//将minweight初试成一个较大的数,之后替换为最小权值for(int k = 1; k < graph.verxes;k++){//有n个顶点,算法结束后会有n-1条边//确定每一次生成的子图,和哪个结点的距离最近for(int i = 0;i < graph.verxes;i++){//i结点表示被访问过的节点for(int j = 0;j < graph.verxes;j++){//j表示还没有被访问过的节点if(visited[i] == 1 && visited[j] == 0 && graph.weight[i][j] < minWeight){//替换minweightminWeight = graph.weight[i][j];h1 = i;h2 = j;}}}System.out.println("边<" + graph.data[h1] + "," + graph.data[h2] + "> 权值:" + minWeight);//将当前找到的节点标记为已经访问visited[h2] = 1;//重置minweightminWeight = 10000;}}
}
class Graph{int verxes;//表示图的节点个数char[] data;//存放节点数据int[][] weight;//存放边,邻接矩阵public Graph(int verxes){this.verxes = verxes;data = new char[verxes];weight = new int[verxes][verxes];}
}

运行结果:
在这里插入图片描述

三、克鲁斯卡尔算法

3.1 克鲁斯卡尔算法的介绍

克鲁斯卡尔(Kruskal)算法,是用来求加权连通图的最小生成树的算法。其基本思想是按照权值从小到大的顺序选择n-1条边,并保证这n-1条边不构成回路

首先构造一个只含n个顶点的森林,然后依权值从小到大从连通网中选择边加入到森林中,并使森林中不产生回路,直至森林变成一棵树为止

3.2 应用场景

有北京有新增7个站点(A, B, C, D, E, F, G) ,现在需要修路把7个站点连通,各个站点的距离用边线表示(权) ,比如 A – B 距离 12公里。问:如何修路保证各个站点都能连通,并且总的修建公路总里程最短?
在这里插入图片描述
基本步骤:

在这里插入图片描述
最后形成一颗最小生成树,它包括的边依次是:<E,F> <C,D> <D,E> <B,F> <E,G> <A,B>

Kruskal算法重点需要解决的两个问题

问题一 : 对图的所有边按照权值大小进行排序。
问题二 : 将边添加到最小生成树中时,怎么样判断是否形成了回路。

问题一,很好解决,采用排序算法进行排序即可。

问题二,处理方式是:记录顶点在”最小生成树”中的终点,顶点的终点是”在最小生成树中与它连通的最大顶点”。然后每次需要将一条边添加到最小生存树时,判断该边的两个顶点的终点是否重合,重合的话则会构成回路。

卡鲁斯卡尔算法的实现最麻烦的就是实现如何判断新添加的边是否和已选择的边构成回路,为此,引入了终点的概念:所有顶点按照从小到大(一般指索引大小)的顺序排列好之后,某个顶点的终点就是 “与它连通的最大顶点”。

下面将以实现过程的第三步为例,具体讲解一下判断回路的思想
在这里插入图片描述

在将<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>加入最小生成树的话, 会形成回路。 这就是判断回路的方式。那么判别是否构成回路的方法就是:如果在加入之前的边的两个顶点的终点是同一个点,则说明出现了回路。

3.3 代码实现

package algorithm.kruskal;import java.util.Arrays;public  class KruskalAlgorithm {private Edata[] edges; // 图中的边private int edgeNum;//边的个数private char[] vertexes;//顶点数组private int[][] matrix;//邻接矩阵private static final int INF = Integer.MAX_VALUE;// 使用INF表示两个顶点不能连通public static void main(String[] args) {char[] vertexes = {'A','B','C','D','E','F','G'};int[][] matrix = {//0表示自己和自己连接,有具体的权值就是能连通,INF表示不能连通/*A*//*B*//*C*//*D*//*E*//*F*//*G*//*A*/ {   0,  12, INF, INF, INF,  16,  14},/*B*/ {  12,   0,  10, INF, INF,   7, INF},/*C*/ { INF,  10,   0,   3,   5,   6, INF},/*D*/ { INF, INF,   3,   0,   4, INF, INF},/*E*/ { INF, INF,   5,   4,   0,   2,   8},/*F*/ {  16,   7,   6, INF,   2,   0,   9},/*G*/ {  14, INF, INF, INF,   8,   9,   0}};KruskalAlgorithm kruskalAlgorithm = new KruskalAlgorithm(vertexes, matrix);kruskalAlgorithm.print();kruskalAlgorithm.kruskal();}public  KruskalAlgorithm(char[] vertexes, int[][] matrix){//初始化顶点数和边的个数int vlen = vertexes.length;//初始化顶点,复制拷贝的方式this.vertexes = new char[vlen];for (int i = 0;i < vertexes.length;i++){this.vertexes[i] = vertexes[i];}//初始化邻接矩阵的行和边this.matrix = new int[vlen][vlen];for (int i = 0;i < vlen;i++){for(int j = 0; j < vlen;j++){this.matrix[i][j] = matrix[i][j];}}//统计边for(int i = 0;i < vlen;i++){for (int j = i + 1;j < vlen;j++){if(this.matrix[i][j] != INF){edgeNum++;}}}//获取图中的边,放到Edata数组中,通过邻接矩阵来获取int index = 0;edges = new Edata[edgeNum];for(int i = 0;i < vertexes.length;i++){for(int j = i + 1;j < vertexes.length;j++){if(this.matrix[i][j] != INF){edges[index++] = new Edata(vertexes[i], vertexes[j], matrix[i][j]);}}}}//生成最小生成树public void kruskal(){int index = 0;//表示最后结果数组的索引int[] end = new int[edgeNum];//用于保存已有生成树的顶点的终点//创建结果Edata[] res = new Edata[edgeNum];//获取图中所有的边的集合,一共有12条边System.out.println("获取图的边的集合" + Arrays.toString(edges) + "共" + edges.length + "条边");//按照边的权值大小排序Arrays.sort(edges);for(int i = 0; i < edgeNum;i++){//获取到第i条边的第一个起点和终点int p1 = getPosition(edges[i].start);int p2 = getPosition(edges[i].end);//获取p1/p2这个顶点在已有最小生成树中的终点int m = getEnd(end,p1);int n = getEnd(end,p2) ;//是否构成回路if(m != n){end[m] = n;//设置m在已有最小生成树的终点res[index++] = edges[i];//有一条边加入到res数组中}}System.out.println("最小生成树为=");for(int i = 0;i < index;i++){System.out.println(res[i]);}}//打印邻接矩阵的方法public void print(){System.out.println("邻接矩阵为");for (int i = 0;i < vertexes.length;i++){for (int j = 0; j < vertexes.length;j++){System.out.printf("%-12d\t",matrix[i][j]);}System.out.println();}}//获取顶点的下标值private int getPosition(char ch){for(int i = 0;i < vertexes.length;i++){if(vertexes[i] == ch){return i;}}return -1;}/*** @param ends 记录各个顶点对应的终点* @param i 表示传入的顶点对应的下标* @return*///获取下标为i的顶点的终点private int getEnd(int[] ends, int i){while(ends[i] != 0){i = ends[i];}return i;}
}//Edata类,它的对象实例就是一条边
class Edata implements Comparable<Edata>{char start;//边的一个点char end;//边的终点int weight;//边的权值//构造器public Edata(char start, char end, int weight) {this.start = start;this.end = end;this.weight = weight;}@Overridepublic String toString() {return"{<" + start +"," + end +">, weight=" + weight + "}";}@Overridepublic int compareTo(Edata o) {return this.weight - o.weight;}
}

运行结果:
在这里插入图片描述

这篇关于最小生成树和普利姆算法及克鲁斯卡尔算法的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/558447

相关文章

MySQL中动态生成SQL语句去掉所有字段的空格的操作方法

《MySQL中动态生成SQL语句去掉所有字段的空格的操作方法》在数据库管理过程中,我们常常会遇到需要对表中字段进行清洗和整理的情况,本文将详细介绍如何在MySQL中动态生成SQL语句来去掉所有字段的空... 目录在mysql中动态生成SQL语句去掉所有字段的空格准备工作原理分析动态生成SQL语句在MySQL

springboot+dubbo实现时间轮算法

《springboot+dubbo实现时间轮算法》时间轮是一种高效利用线程资源进行批量化调度的算法,本文主要介绍了springboot+dubbo实现时间轮算法,文中通过示例代码介绍的非常详细,对大家... 目录前言一、参数说明二、具体实现1、HashedwheelTimer2、createWheel3、n

Java利用docx4j+Freemarker生成word文档

《Java利用docx4j+Freemarker生成word文档》这篇文章主要为大家详细介绍了Java如何利用docx4j+Freemarker生成word文档,文中的示例代码讲解详细,感兴趣的小伙伴... 目录技术方案maven依赖创建模板文件实现代码技术方案Java 1.8 + docx4j + Fr

Java编译生成多个.class文件的原理和作用

《Java编译生成多个.class文件的原理和作用》作为一名经验丰富的开发者,在Java项目中执行编译后,可能会发现一个.java源文件有时会产生多个.class文件,从技术实现层面详细剖析这一现象... 目录一、内部类机制与.class文件生成成员内部类(常规内部类)局部内部类(方法内部类)匿名内部类二、

使用Jackson进行JSON生成与解析的新手指南

《使用Jackson进行JSON生成与解析的新手指南》这篇文章主要为大家详细介绍了如何使用Jackson进行JSON生成与解析处理,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1. 核心依赖2. 基础用法2.1 对象转 jsON(序列化)2.2 JSON 转对象(反序列化)3.

java中使用POI生成Excel并导出过程

《java中使用POI生成Excel并导出过程》:本文主要介绍java中使用POI生成Excel并导出过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录需求说明及实现方式需求完成通用代码版本1版本2结果展示type参数为atype参数为b总结注:本文章中代码均为

在java中如何将inputStream对象转换为File对象(不生成本地文件)

《在java中如何将inputStream对象转换为File对象(不生成本地文件)》:本文主要介绍在java中如何将inputStream对象转换为File对象(不生成本地文件),具有很好的参考价... 目录需求说明问题解决总结需求说明在后端中通过POI生成Excel文件流,将输出流(outputStre

SpringBoot实现MD5加盐算法的示例代码

《SpringBoot实现MD5加盐算法的示例代码》加盐算法是一种用于增强密码安全性的技术,本文主要介绍了SpringBoot实现MD5加盐算法的示例代码,文中通过示例代码介绍的非常详细,对大家的学习... 目录一、什么是加盐算法二、如何实现加盐算法2.1 加盐算法代码实现2.2 注册页面中进行密码加盐2.

Java时间轮调度算法的代码实现

《Java时间轮调度算法的代码实现》时间轮是一种高效的定时调度算法,主要用于管理延时任务或周期性任务,它通过一个环形数组(时间轮)和指针来实现,将大量定时任务分摊到固定的时间槽中,极大地降低了时间复杂... 目录1、简述2、时间轮的原理3. 时间轮的实现步骤3.1 定义时间槽3.2 定义时间轮3.3 使用时

C/C++随机数生成的五种方法

《C/C++随机数生成的五种方法》C++作为一种古老的编程语言,其随机数生成的方法已经经历了多次的变革,早期的C++版本使用的是rand()函数和RAND_MAX常量,这种方法虽然简单,但并不总是提供... 目录C/C++ 随机数生成方法1. 使用 rand() 和 srand()2. 使用 <random