七.克鲁斯卡尔(Kruskal)算法
文章目录
- 克鲁斯卡尔算法
- 应用场景—公交站问题
- 克鲁斯卡尔算法介绍
- 克鲁斯卡尔算法图解说明
- 克鲁斯卡尔算法分析
- 如何判断是否构成回路—举例说明
- 代码实现
- 第一步 先构建图
- 最终代码
克鲁斯卡尔算法
应用场景—公交站问题
看一个应用场景和问题
1)某城市新增7个站点(A,B,C,D,E,F,G),现在需要修路把7个站点连通
2)各个站点的距离用边线表示(权),比如A—B距离12公里
3)问:如何修路保证各个站点都能连通,并且总的修建公路总里程最短?
克鲁斯卡尔算法介绍
1)克鲁斯卡尔(Kruskal)算法,是用来求加权连通图的最小生成树的算法
2)基本思想:按照权值从小到大的顺序选择n-1条边,并保证这n-1条边不构成回路
3)具体做法:首先构造一个只含n个顶点的森林,然后依权值从小到大从连通网中选择边加入到森林中,并使森林中不产生回路,直至森林变成一棵树为止。
克鲁斯卡尔算法图解说明
以城市公交站问题来图解说明 克鲁斯卡尔算法的原理和步骤:
此时,最小生成树构造完成,它包括的边依次是:<E,F> <C,D> <D,E> <B,F> <E,G> <A,B>
克鲁斯卡尔算法分析
克鲁斯卡尔算法重点需要解决以下两个问题:
问题一:对图的所有边按照权值大小进行排序。
问题二:将边添加到最小生成树时,怎么判断是否形成了回路
问题一很好解决,采用排序算法进行排序即可。
问题二处理方式是:记录顶点在“最小生成树”中的终点,顶点的终点是“在最小生成树中与它连通的最大顶点”。然后每次需要将一条边添加到最小生成树时,判断该边的两个顶点的终点是否重合,重合的话则会构成回路。
如何判断是否构成回路—举例说明
在将<E,F> <C,D> <D,E>加入到最小生成树R中之后,这几条边的顶点就都有了终点:
(1) C的终点是F
(2) D的终点是F
(3) E的终点是F
(4) F的终点是F
关于终点的说明:就是将所有顶点按照从小到大的顺序排列好之后,某个顶点的终点就是“与它连通的最大顶点”。因此,虽然<C,E>是权值最小的边,但是C和E得终点都是F,即它们的终点相同。因此,将<C,E>加入最小生成树的话,会形成回路。这就是判断回路的方式。也就是说,我们加入的边的两个顶点不能都指向同一个终点,否则将构成回路。(后面有代码说明)
代码实现
第一步 先构建图
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace KruskalCase
{class Kruskal{private int edgeNum; //记录边的个数private char[] vertexs; //顶点数组private int[,] matrix; //存储图的邻接矩阵private static int INF = 999; //使用999表示两个顶点不能连通static void Main(string[] args){char[] vertexs = { 'A', 'B', 'C', 'D', 'E','F', 'G' }; //七个顶点// 邻接矩阵 0表示相同的点间的距离, INF表示不是直连int[,] matrix ={{0,12,INF,INF,INF,16,14 },{12,0,10,INF,INF,7,INF },{INF,10,0,3,5,6,INF},{INF,INF,3,0,4,INF,INF },{INF,INF,5,4,0,2,8 },{16,7,6,INF,2,0,9 },{14,INF,INF,INF,8,9,0 }};//创建Kruskal对象实例Kruskal kruskal = new Kruskal(vertexs, matrix);//输出kruskal.print();}//构造器public Kruskal(char[] vertexs,int[,] matrix){//初始化顶点数int vlen = vertexs.Length;//初始化顶点,复制拷贝的方式this.vertexs = new char[vlen];for (int i = 0; i < vertexs.Length; i++){this.vertexs[i] = vertexs[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 = 0; j < vlen; j++){if (this.matrix[i, j] != INF){edgeNum++;}}}}//打印邻接矩阵public void print(){Console.WriteLine("邻接矩阵为:\n");for (int i = 0; i < vertexs.Length; i++){for (int j = 0; j < vertexs.Length; j++){Console.Write("{0,5}"+matrix[i,j]);}Console.WriteLine(); //换行}}}
}
最终代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace KruskalCase
{class Kruskal{private int edgeNum; //记录边的个数private char[] vertexs; //顶点数组private int[,] matrix; //存储图的邻接矩阵private static int INF = 999; //使用999表示两个顶点不能连通static void Main(string[] args){char[] vertexs = { 'A', 'B', 'C', 'D', 'E','F', 'G' }; //七个顶点// 邻接矩阵 0表示相同的点间的距离, INF表示不是直连int[,] matrix ={{0,12,INF,INF,INF,16,14 },{12,0,10,INF,INF,7,INF },{INF,10,0,3,5,6,INF},{INF,INF,3,0,4,INF,INF },{INF,INF,5,4,0,2,8 },{16,7,6,INF,2,0,9 },{14,INF,INF,INF,8,9,0 }};//创建Kruskal对象实例Kruskal kruskal = new Kruskal(vertexs, matrix);//打印邻接矩阵kruskal.print();//EData[] edges = kruskal.getEdges();////把图中的边打印出来,排序前//Console.WriteLine("排序前:");//foreach (var item in edges)//{// Console.Write(item+" ");//}//Console.WriteLine("\n"); //换行//Console.WriteLine("排序后:");//kruskal.sortEdges(edges); ////对边进行排序后,把边打印出来//foreach (var item in edges)//{// Console.Write(item+" ");//}kruskal.kruskalalgorithm();}//构造器public Kruskal(char[] vertexs,int[,] matrix){//初始化顶点数int vlen = vertexs.Length;//初始化顶点,复制拷贝的方式this.vertexs = new char[vlen];for (int i = 0; i < vertexs.Length; i++){this.vertexs[i] = vertexs[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++;}}}}//克鲁斯卡尔算法得到最小生成树public void kruskalalgorithm(){int index = 0; //表示最后结果数组的索引int[] ends = new int[edgeNum]; //用于保存"已有最小生成树"中的每个顶点在最小生成树中的终点//创建结果数组,保存最后的最小生成树EData[] rets = new EData[edgeNum];//获取图中所有的边的集合,一共有12条边EData[] edges = getEdges();Console.WriteLine("图的边的集合:");foreach (var item in edges){Console.WriteLine(item+" ");}Console.WriteLine("共有:"+edges.Count()+"条边"); //12Console.WriteLine(); //首先按照边的权值大小进行排序(从小到大)sortEdges(edges);//遍历edges数组,将边添加到最小生成树中时,判断准备加入的边是否形成了回路,如果没有,就加入rets,否则不能加入for(int i = 0; i < edgeNum; i++){//获取到第i条边的第一个顶点(起点)int p1 = getPosition(edges[i].start); //p1=4//获取到第i条边的第二个顶点int p2 = getPosition(edges[i].end); //p2=5//获取p1这个顶点在已有的最小生成树中的终点int m = getEnd(ends, p1); //m=4 未加入的顶点,终点就是它本身//获取p2这个顶点在已有的最小生成树中的终点int n = getEnd(ends, p2); //n=5//是否构成回路if (m != n) //没有构成回路{ends[m] = n; //设置m在"已有最小生成树"中的终点为n,以边<E,F>举例,E的终点是Frets[index++] = edges[i]; //有一条边加入到rets数组}}//打印“最小生成树”Console.WriteLine("最小生成树为:");foreach (var item in rets){Console.WriteLine(item+" ");}}//打印邻接矩阵public void print(){Console.WriteLine("邻接矩阵为:\n");for (int i = 0; i < vertexs.Length; i++){for (int j = 0; j < vertexs.Length; j++){Console.Write("{0,5}",matrix[i,j]); //5表示5个占位符,-表示从左侧对齐,没有负号表示从右侧对齐}Console.WriteLine(); //换行} }/// <summary>/// 对边进行排序处理,冒泡排序(对图的所有边按照权值大小进行排序。)/// </summary>/// <param name="edges">边的集合</param>private void sortEdges(EData[] edges){for (int i = 0; i < edges.Length-1; i++){for (int j = 0; j < edges.Length-1-i; j++){if (edges[j].weight > edges[j + 1].weight){//交换EData tmp = edges[j];edges[j] = edges[j + 1];edges[j + 1] = tmp;}}}}/// <summary>/// 获取顶点的下标/// </summary>/// <param name="ch">顶点的值,比如'A','B'</param>/// <returns>返回ch顶点对应的下标,如果找不到,返回-1</returns>private int getPosition(char ch){for (int i = 0; i < vertexs.Length; i++){ if (vertexs[i] == ch) //找到{return i;}}//找不到,返回-1return -1;}/// <summary>/// 获取图中的边,放到EData[]数组中,后面我们需要遍历该数组/// 是通过matrix 邻接矩阵来获取边的权值/// EData[]形式 [['A','B',12], ['B','F',7],...]/// </summary>/// <returns></returns>private EData[] getEdges(){int index = 0;EData[] edges = new EData[edgeNum]; //edgeNum:边的条数for (int i = 0; i < vertexs.Length; i++){for (int j =i+1; j < vertexs.Length; j++){if (matrix[i, j] != INF){edges[index++] = new EData(vertexs[i], vertexs[j], matrix[i, j]);}}}return edges;}/// <summary>/// 功能:获取下标为i的顶点的终点,用于后面判断两个顶点的终点是否相同,用于判断权值最小的边是否能加入到最小生成树的方法/// </summary>/// <param name="ends">ends数组就是记录了各个顶点对应的终点下标是哪个,ends数组是在遍历过程中,逐步形成的</param>/// <param name="i">传入的顶点对应的下标</param>/// <returns>返回的就是下标为i的这个顶点对应的终点的下标</returns>private int getEnd(int[] ends,int i){while (ends[i] != 0) //哪个顶点的终点是哪一个是动态加入的(如果终点不等于零则返回终点,如果终点等于零则返回自己下标){i = ends[i];}return i;}}//创建一个类EData,它的对象实例就表示一条边class EData{public char start; //边的一个点public char end; //边的另外一个点public int weight; //边的权值//构造器public EData(char start,char end,int weight){this.start = start;this.end = end;this.weight = weight;}//重写Tostring方法,便于输出边public override string ToString(){return "EData [start="+start+ ", end="+end +" ,weight="+weight+"]";}}
}