克鲁斯卡尔算法介绍

  • 克鲁斯卡尔(Kruskal)算法,是用来求加权连通图的最小生成树的算法
  • 基本思想:按照权值从小到大的顺序选择n-1条边,并保证这n-1条边不构成回路
  • 具体做法:首先构造一个只含n个顶点的森林,然后依权值从小到大从连通网中选择边加入到森林中,并使森林中不产生回路,直至森林变成一棵树为止

    问题描述

    image.png

  • 某城市新增7个站点(A, B, C, D, E, F, G) ,现在需要修路把7个站点连通

  • 各个站点的距离用边线表示(权) ,比如 A – B 距离 12公里
  • 问:如何修路保证各个站点都能连通,并且总的修建公路总里程最短?

图解

  1. 把边的权值排好序,选择权值最小的一条边,[2]

image.png

  1. 继续选择剩余的边,权值最小的,[3]

image.png

  1. 继续选择剩余权值最小的边,[4]

image.png

  1. 下一个权值最小的是[5],但是C的终点是F,E的终点也是F,也就是说CE的终点相同,构成回路,所以不能选择CE
  2. 继续下一个,[6],同样,CF的终点都是F,构成回路
  3. 下一个[7],BF的终点不相同,不构成回路,所以选择BF

image.png

  1. 如上的选择方式同理,选择剩下的部分

image.png
image.png

代码

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Globalization;
  4. using System.IO;
  5. using System.Runtime.Serialization.Formatters.Binary;
  6. using System.Text;
  7. namespace ConsoleApp1
  8. {
  9. class Program
  10. {
  11. static void Main(string[] args)
  12. {
  13. char[] vertexs = new char[] { 'A', 'B', 'C', 'D', 'E', 'F', 'G' };
  14. //邻接矩阵的关系使用二维数组
  15. //比如C和G之间不连通,就用8888来表示,权值太大就不考虑了
  16. int x = 8888;
  17. int[,] matrix = new int[,]
  18. {
  19. //A B C D E F G
  20. /*A*/{0 , 12, x , x , x , 16, 14 },
  21. /*B*/{12, 0 , 10, x , x , 7 , x },
  22. /*C*/{x , 10, 0 , 3 , 5 , 6 , x },
  23. /*D*/{x , x , 3 , 0 , 4 , x , x },
  24. /*E*/{x , x , 5 , 4 , 0 , 2 , 8 },
  25. /*F*/{16, 7 , 6 , x , 2 , 0 , 9 },
  26. /*G*/{14, x , x , x , 8 , 9 , 0 }
  27. };
  28. KruskalCase kruskal=new KruskalCase(vertexs, matrix);
  29. kruskal.print();
  30. Console.WriteLine("没排序");
  31. EData[] edatas = kruskal.getEdges();
  32. foreach(var edata in edatas)
  33. {
  34. edata.print();
  35. }
  36. Console.WriteLine("排序完");
  37. kruskal.sortEdges(edatas);
  38. foreach (var edata in edatas)
  39. {
  40. edata.print();
  41. }
  42. kruskal.kruskal();
  43. }
  44. }
  45. class KruskalCase
  46. {
  47. private int edgeNum;//边的个数
  48. private char[] vertexs;//顶点数组
  49. private int[,] matrix;//邻接矩阵
  50. public KruskalCase(char[] vertexs,int[,] matrix)
  51. {
  52. this.vertexs = vertexs;
  53. this.matrix = matrix;
  54. //统计边的数量,j=i+1,自己跟自己就没有必要再进行连接了
  55. for(int i = 0; i < matrix.GetLength(0); i++)
  56. {
  57. for(int j = i+1; j < matrix.GetLength(1); j++)
  58. {
  59. if (this.matrix[i, j] != 8888)
  60. {
  61. edgeNum++;
  62. }
  63. }
  64. }
  65. }
  66. //克鲁斯卡尔算法
  67. public void kruskal()
  68. {
  69. int index = 0;//最后结果数组的索引
  70. int[] ends = new int[vertexs.Length];//用于保存 "已有最小生成" 中的每个顶点在最小生成树中的终点
  71. //创建结果数组,保存最后的最小生成树
  72. EData[] rets=new EData[vertexs.Length-1];
  73. //获取图中所有边的集合
  74. EData[] edges = getEdges();
  75. //按照边的权值大小排序
  76. sortEdges(edges);
  77. //遍历edges数组,将边添加到最小生成树中时,判断准备加入的边是否构成了回路,如果没有就加入rets,否则就不能加入
  78. for(int i = 0; i < edgeNum; i++)
  79. {
  80. //获取到第i条边的第一个顶点(起点),第二个顶点(终点)
  81. int p1 = getPosition(edges[i].start);
  82. int p2 = getPosition(edges[i].end);
  83. //获取p1,p2这个顶点在已有的最小生成树中的终点
  84. int m = getEnd(ends, p1);
  85. int n = getEnd(ends, p2);
  86. //判断是否构成回路
  87. if (m != n)//两条边的终点不一样就没有构成回路
  88. {
  89. ends[m] = n;//设置m在已有最小生成树的终点
  90. rets[index++] = edges[i];//有一条边入选了
  91. }
  92. }
  93. Console.WriteLine("最小生成树");
  94. foreach (var ret in rets)
  95. {
  96. ret.print();
  97. }
  98. }
  99. //打印邻接矩阵
  100. public void print()
  101. {
  102. for (int i = 0; i < matrix.GetLength(0); i++)
  103. {
  104. for (int j = 0; j < matrix.GetLength(1); j++)
  105. Console.Write(matrix[i, j] + " ");
  106. Console.WriteLine();
  107. }
  108. }
  109. //对边进行排序处理,这里用冒泡
  110. public void sortEdges(EData[] edges)
  111. {
  112. for(int i = 0; i < edges.Length; i++)
  113. {
  114. for (int j = 0; j < edges.Length - 1 - i; j++)
  115. {
  116. if (edges[j].weight > edges[j + 1].weight)//交换
  117. {
  118. EData temp = edges[j];
  119. edges[j] = edges[j + 1];
  120. edges[j+1] = temp; ;
  121. }
  122. }
  123. }
  124. }
  125. //返回ch顶点对应的下标,找不到返回-1
  126. public int getPosition(char ch)
  127. {
  128. for(int i =0;i<vertexs.Length;i++)
  129. if (vertexs[i] == ch)
  130. return i;
  131. return -1;
  132. }
  133. //获取图中的边,放到EData[]数组中,后面需要遍历该数组
  134. //通过matrix邻接矩阵来获取
  135. public EData[] getEdges()
  136. {
  137. int index = 0;
  138. EData[] edges = new EData[edgeNum];
  139. for(int i = 0; i < vertexs.Length; i++)
  140. {
  141. for(int j = i + 1; j < vertexs.Length; j++)
  142. {
  143. if (matrix[i, j] != 8888)
  144. {
  145. edges[index++] = new EData(vertexs[i], vertexs[j], matrix[i, j]);
  146. }
  147. }
  148. }
  149. return edges;
  150. }
  151. //获取下标为i的顶点的终点,用于后面判断两个顶点的终点是否相同
  152. //ends记录了各个顶点对应的终点是哪个,ends是在遍历过程中逐步行程的
  153. //i表示传入的顶点对应的下标
  154. //返回i这个顶点对应的终点的下标
  155. public int getEnd(int[] ends,int i)
  156. {
  157. while (ends[i] != 0)
  158. i = ends[i];
  159. return i;
  160. /*
  161. * 一开始ends这个数组是这样的[0,0,0,0,0,0,0]
  162. * i表示传入的顶点的下标,如果这个下标为0,说明这个顶点还没有终点,为0都不进while,直接返回这个下标
  163. * 比如EF这条边,
  164. * m = E这个点下标为4,最开始传入的时候ends[4]==0,所以直接返回4出去
  165. * n = F这个点下标为5,直接返回5
  166. * m和n不相同,所以不构成回路,这条边可以加入,加入后,就要设置ends,ends[4]=5
  167. * [0,0,0,0,5,0,0]
  168. * 表示什么意思,第4个顶点,就是E的终点是5,5指向F,所以E的终点是F。
  169. * 下次检测的时候,如果传入的是4,返现ends[4]!=0,说明设置了终点,就把终点的下标赋值给当前下标,循环检测
  170. */
  171. }
  172. }
  173. //边
  174. class EData
  175. {
  176. public char start;//边的一个点
  177. public char end;//边的另一个点
  178. public int weight;//边的权值
  179. public EData(char start,char end,int weight)
  180. {
  181. this.start = start;
  182. this.end = end;
  183. this.weight = weight;
  184. }
  185. public void print()
  186. {
  187. Console.WriteLine($"起始{start},结束{end},权值{weight}");
  188. }
  189. }
  190. }