克鲁斯卡尔算法介绍
- 克鲁斯卡尔(Kruskal)算法,是用来求加权连通图的最小生成树的算法
- 基本思想:按照权值从小到大的顺序选择n-1条边,并保证这n-1条边不构成回路
具体做法:首先构造一个只含n个顶点的森林,然后依权值从小到大从连通网中选择边加入到森林中,并使森林中不产生回路,直至森林变成一棵树为止
问题描述
某城市新增7个站点(A, B, C, D, E, F, G) ,现在需要修路把7个站点连通
- 各个站点的距离用边线表示(权) ,比如 A – B 距离 12公里
- 问:如何修路保证各个站点都能连通,并且总的修建公路总里程最短?
图解
- 把边的权值排好序,选择权值最小的一条边,
[2]
- 继续选择剩余的边,权值最小的,
[3]
- 继续选择剩余权值最小的边,
[4]
- 下一个权值最小的是
[5],但是C的终点是F,E的终点也是F,也就是说CE的终点相同,构成回路,所以不能选择CE - 继续下一个,
[6],同样,CF的终点都是F,构成回路 - 下一个
[7],BF的终点不相同,不构成回路,所以选择BF
- 如上的选择方式同理,选择剩下的部分
代码
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
char[] vertexs = new char[] { 'A', 'B', 'C', 'D', 'E', 'F', 'G' };
//邻接矩阵的关系使用二维数组
//比如C和G之间不连通,就用8888来表示,权值太大就不考虑了
int x = 8888;
int[,] matrix = new int[,]
{
//A B C D E F G
/*A*/{0 , 12, x , x , x , 16, 14 },
/*B*/{12, 0 , 10, x , x , 7 , x },
/*C*/{x , 10, 0 , 3 , 5 , 6 , x },
/*D*/{x , x , 3 , 0 , 4 , x , x },
/*E*/{x , x , 5 , 4 , 0 , 2 , 8 },
/*F*/{16, 7 , 6 , x , 2 , 0 , 9 },
/*G*/{14, x , x , x , 8 , 9 , 0 }
};
KruskalCase kruskal=new KruskalCase(vertexs, matrix);
kruskal.print();
Console.WriteLine("没排序");
EData[] edatas = kruskal.getEdges();
foreach(var edata in edatas)
{
edata.print();
}
Console.WriteLine("排序完");
kruskal.sortEdges(edatas);
foreach (var edata in edatas)
{
edata.print();
}
kruskal.kruskal();
}
}
class KruskalCase
{
private int edgeNum;//边的个数
private char[] vertexs;//顶点数组
private int[,] matrix;//邻接矩阵
public KruskalCase(char[] vertexs,int[,] matrix)
{
this.vertexs = vertexs;
this.matrix = matrix;
//统计边的数量,j=i+1,自己跟自己就没有必要再进行连接了
for(int i = 0; i < matrix.GetLength(0); i++)
{
for(int j = i+1; j < matrix.GetLength(1); j++)
{
if (this.matrix[i, j] != 8888)
{
edgeNum++;
}
}
}
}
//克鲁斯卡尔算法
public void kruskal()
{
int index = 0;//最后结果数组的索引
int[] ends = new int[vertexs.Length];//用于保存 "已有最小生成" 中的每个顶点在最小生成树中的终点
//创建结果数组,保存最后的最小生成树
EData[] rets=new EData[vertexs.Length-1];
//获取图中所有边的集合
EData[] edges = getEdges();
//按照边的权值大小排序
sortEdges(edges);
//遍历edges数组,将边添加到最小生成树中时,判断准备加入的边是否构成了回路,如果没有就加入rets,否则就不能加入
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(ends, p1);
int n = getEnd(ends, p2);
//判断是否构成回路
if (m != n)//两条边的终点不一样就没有构成回路
{
ends[m] = n;//设置m在已有最小生成树的终点
rets[index++] = edges[i];//有一条边入选了
}
}
Console.WriteLine("最小生成树");
foreach (var ret in rets)
{
ret.print();
}
}
//打印邻接矩阵
public void print()
{
for (int i = 0; i < matrix.GetLength(0); i++)
{
for (int j = 0; j < matrix.GetLength(1); j++)
Console.Write(matrix[i, j] + " ");
Console.WriteLine();
}
}
//对边进行排序处理,这里用冒泡
public void sortEdges(EData[] edges)
{
for(int i = 0; i < edges.Length; i++)
{
for (int j = 0; j < edges.Length - 1 - i; j++)
{
if (edges[j].weight > edges[j + 1].weight)//交换
{
EData temp = edges[j];
edges[j] = edges[j + 1];
edges[j+1] = temp; ;
}
}
}
}
//返回ch顶点对应的下标,找不到返回-1
public int getPosition(char ch)
{
for(int i =0;i<vertexs.Length;i++)
if (vertexs[i] == ch)
return i;
return -1;
}
//获取图中的边,放到EData[]数组中,后面需要遍历该数组
//通过matrix邻接矩阵来获取
public EData[] getEdges()
{
int index = 0;
EData[] edges = new EData[edgeNum];
for(int i = 0; i < vertexs.Length; i++)
{
for(int j = i + 1; j < vertexs.Length; j++)
{
if (matrix[i, j] != 8888)
{
edges[index++] = new EData(vertexs[i], vertexs[j], matrix[i, j]);
}
}
}
return edges;
}
//获取下标为i的顶点的终点,用于后面判断两个顶点的终点是否相同
//ends记录了各个顶点对应的终点是哪个,ends是在遍历过程中逐步行程的
//i表示传入的顶点对应的下标
//返回i这个顶点对应的终点的下标
public int getEnd(int[] ends,int i)
{
while (ends[i] != 0)
i = ends[i];
return i;
/*
* 一开始ends这个数组是这样的[0,0,0,0,0,0,0]
* i表示传入的顶点的下标,如果这个下标为0,说明这个顶点还没有终点,为0都不进while,直接返回这个下标
* 比如EF这条边,
* m = E这个点下标为4,最开始传入的时候ends[4]==0,所以直接返回4出去
* n = F这个点下标为5,直接返回5
* m和n不相同,所以不构成回路,这条边可以加入,加入后,就要设置ends,ends[4]=5
* [0,0,0,0,5,0,0]
* 表示什么意思,第4个顶点,就是E的终点是5,5指向F,所以E的终点是F。
* 下次检测的时候,如果传入的是4,返现ends[4]!=0,说明设置了终点,就把终点的下标赋值给当前下标,循环检测
*/
}
}
//边
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;
}
public void print()
{
Console.WriteLine($"起始{start},结束{end},权值{weight}");
}
}
}