一个连通图的生成树是一个极小的连通子图,它含有图中全部的顶点,但只有足以构成一棵树的n-1条边。
我们把构造连通网的最小代价生成树称为最小生成树(MinimumCostSpanningTree)
找连通网的最小生成树,经典的算法有两种:普里姆算法和克鲁斯卡尔算法。
普里姆算法:适合稠密图,边数非常多的情况
克鲁斯卡尔算法:适合稀疏图,边数少效率会非常高
1.普里姆(Prim)算法:以某顶点为起点,逐步找各顶点上最小权值的边来构建最小生成树
首先:有一个权值数组(初始化为最大值)、一个索引数组(初始化为0,表示从第一个顶点开始)
总的思想:先从一个顶点开始,读取对应顶点那一行的权值(该顶点和那些点连通),找最小权值,找到该顶点对应的邻接点,然后在读取该邻接点那一行的权值(权值数组只保留最小值,权值为0表示该点已经是最小生成树中的点,不再参与比较,索引数组更新为该邻接点的下标)
//测试数据
int[] vertexs = new int[] { 0,1,2,3,4,5,6,7,8};
int[,] edges = new int[,]
{
//0 1 2 3 4 5 6 7 8
{ 0, 10, MAX, MAX, MAX, 11, MAX, MAX, MAX, }, //0
{ 10, 0, 18, MAX, MAX, MAX, 16, MAX, 12, }, //1
{ MAX, 18, 0, 22, MAX, MAX, MAX, MAX, 8, }, //2
{ MAX, MAX, 22, 0, 20, MAX, 24, 16, 21, }, //3
{ MAX, MAX, MAX, 20, 0, 26, MAX, 7, MAX, }, //4
{ 11, MAX, MAX, MAX, 26, 0, 17, MAX, MAX, }, //5
{ 16, MAX, MAX, 24, MAX, 17, 0, 19, MAX, }, //6
{ MAX, MAX, MAX, 16, 7, MAX, 19, 0, MAX, }, //7
{ MAX, 12, 8, 21, MAX, MAX, MAX, MAX, 0, }, //8
};
过程:
从第一个顶点开始V0,即数组第一个,遍历从数组索引1开始:
1.先把第一个顶点放入生成树里,所以从读取第一行的权值数据,索引数组全为0,因为是V0顶点到其它顶点的边
{ 0, 10, MAX, MAX, MAX, 11, MAX, MAX, MAX, }, //weight 0
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, }, //index
//遍历数组得:min=10, 对应的边:0-1(V0-V1)
2.然后读取顶点V1那一行(第二行)的权值数据放入数组,只放入比数组现有值小的权值,然后修改索引数组的值为相应的下标,从哪一行读取的,下标就是那一行的下标,数组中为0的位置表示已经是最小生成树里的点了,不再参与。
{ 0, 0, 18, MAX, MAX, 11, 16, MAX, 12, }, //weight
{ 0, 0, 1, 0, 0, 0, 1, 0, 1, }, //index
//min=11 0-5 (11在数组中下标5,同样下标中的索引数组的值是0,所以是0-5) 读取顶点V5的数据放入数组
{ 0, 0, 18, MAX, 26, 0, 16, MAX, 12, }, //weight
{ 0, 0, 1, 0, 0, 5, 1, 0, 1, }, //index
//min=12 1-8 读取顶点V8的数据放入数组
{ 0, 0, 8, 21, 26, 0, 16, MAX, 0, }, //weight
{ 0, 0, 8, 8, 0, 5, 1, 0, 1, }, //index
//min=8 8-2 读取顶点V2的数据放入数组
{ 0, 0, 0, 21, 26, 0, 16, MAX, 0, }, //weight
{ 0, 0, 8, 8, 0, 5, 1, 0, 1, }, //index
//min=16 1-6 读取顶点V6的数据放入数组
{ 0, 0, 0, 21, 26, 0, 0, 19, 0, }, //weight
{ 0, 0, 8, 8, 0, 5, 1, 6, 1, }, //index
//min=19 6-7 读取顶点V7的数据放入数组
{ 0, 0, 0, 16, 7, 0, 0, 0, 0, }, //weight
{ 0, 0, 8, 7, 7, 5, 1, 6, 1, }, //index
//min=7 7-4 读取顶点V4的数据放入数组
{ 0, 0, 0, 16, 0, 0, 0, 0, 0, }, //weight
{ 0, 0, 8, 7, 7, 5, 1, 6, 1, }, //index
//min=16 7-3
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, }, //weight
{ 0, 0, 8, 7, 7, 5, 1, 6, 1, }, //index
/// <summary>
/// 普里姆算法:最小生成树
/// </summary>
public void MiniSpanTree_Prim()
{
int minWeight, i, j, minWeightIndex;
int[] adjvex = new int[vertexs.Length]; //保存相关顶点下标
int[] lowWeight = new int[vertexs.Length]; //保存相关顶点间边的权值
lowWeight[0] = 0; //初始化第一个权值为0,即V0加入生成树,lowCost的值为0,在这里就是此下标的顶点已经加入生成树
adjvex[0] = 0; //初始化第一个顶点下标为0,从顶点V0开始
//读取第一行,初始化
for (i = 1; i < vertexs.Length; i++) //循环除下标为0外的全部顶点
{
lowWeight[i] = edges[0, i]; //将V0顶点与之有边的权值存入数组
adjvex[i] = 0; //初始化都为V0的下标
}
//构造最小生成树
for (i = 1; i < vertexs.Length; i++)
{
minWeight = int.MaxValue; //初始化最小权值为极大值,通常设置为不可能的大数字,如:32765、65535等
j = 1; //顶点下标循环变量
minWeightIndex = 0; //用来存储最小权值的顶点下标
//遍历每一行的数据
while (j < vertexs.Length)
{
if (lowWeight[j] != 0 && lowWeight[j] < minWeight)
{
minWeight = lowWeight[j]; //让当前权值成为最小值
minWeightIndex = j; //将当前最小值的下标存入k
}
j++;
}
Debug.Log("打印当前顶点边中权值最小边:" + adjvex[minWeightIndex] + "---" + minWeightIndex);
lowWeight[minWeightIndex] = 0; //将当前顶点的权值设置为0,表示此顶点已经完成任务
for (j = 1; j < vertexs.Length; j++) //循环所有顶点
{
if (lowWeight[j] != 0 && edges[minWeightIndex, j] < lowWeight[j])
{
//若下标为k顶点各边权值小于此前这些顶点未被加入生成树权值
lowWeight[j] = edges[minWeightIndex, j]; //将较小权值存入lowCost
adjvex[j] = minWeightIndex; //将下标为k的顶点存入adjvex
}
}
}
}
2.克鲁斯卡尔(Kruskal)算法:以边为目标去构建,因为权值在边上,直接去找最小权值的边来构建生成树也是很自然的想法,只不过构建时要考虑是否会形成环路而已。
用到图的存储结构中的边集数组结构
/// <summary>
/// 克鲁斯卡尔算法:最小生成树
/// </summary>
/// <param name="graph">邻接矩阵</param>
public void MiniSpanTree_Kruskal()
{
int i, n, m;
int[] parent = new int[vertexArr.Length]; //定义一数组用来判断边与边是否形成环路
for (i = 0; i < vertexArr.Length; i++)
{
parent[i] = 0; //初始化数组值为0
}
for (i = 0; i < edgeList.Count; i++) //循环每一条边
{
n = Find(parent, edgeList[i].begin);
m = Find(parent, edgeList[i].end);
if (n != m) //假如n与m不等,说明此边没有与现有生成树形成环路
{
parent[n] = m; //将此边的结尾顶点放入下标为起点的parent中,表示此顶点已经在生成树集合中
Debug.Log("(" + edgeList[i].begin + "," + edgeList[i].end + ") " + edgeList[i].weight);
}
}
}
//查找连线顶点的尾部下标
private int Find(int[] parent, int f)
{
while (parent[f] > 0)
f = parent[f];
return f;
}
完整代码:
邻接矩阵最小生成树:看最后的一部分代码即可
/// <summary>
/// 泛型邻接矩阵
/// </summary>
public class AdjacentMatrix<T>
{
#region 数据
//数据
private T[] vertexs; //顶点数组:数据
private int[,] edges; //边数组:权值
private int maxNumVertex; //最大顶点数
#endregion
#region 操作
/// <summary>
/// 创建无向网图的邻接矩阵表示
/// </summary>
/// <param name="vertexs">顶点数据</param>
/// <param name="edges">边数据:权值</param>
public AdjacentMatrix(T[] vertexs, int[,] edges)
{
maxNumVertex = vertexs.Length;
this.vertexs = new T[maxNumVertex];
this.edges = new int[maxNumVertex, maxNumVertex];
Array.Copy(vertexs, this.vertexs, vertexs.Length);
Array.Copy(edges, this.edges, edges.Length);
}
/// <summary>
/// 创建无向网图的邻接矩阵表示
/// </summary>
/// <param name="vertexs">顶点数据</param>
/// <param name="edges">边数据:权值</param>
/// <param name="maxNumVertex">最大顶点数组容量</param>
public AdjacentMatrix(T[] vertexs, int[,] edges, int maxNumVertex)
{
maxNumVertex = maxNumVertex < vertexs.Length ? vertexs.Length : maxNumVertex;
this.vertexs = new T[maxNumVertex];
this.edges = new int[maxNumVertex, maxNumVertex];
this.maxNumVertex = maxNumVertex;
Array.Copy(vertexs, this.vertexs, vertexs.Length);
Array.Copy(edges, this.edges, edges.Length);
}
private bool[] visited;
/// <summary>
/// 邻接矩阵的深度优先遍历
/// </summary>
/// <param name="gl"></param>
public void DFSTraverse()
{
visited = new bool[maxNumVertex];
for (int i = 0; i < vertexs.Length; i++)
{
visited[i] = false; //初始化所有顶点都是未访问的状态
}
for (int i = 0; i < vertexs.Length; i++)
{
//对未访问过的顶点调用DFS,若是连通图,只会执行一次
if (!visited[i])
{
DFS(i);
}
}
}
/// <summary>
/// 邻接矩阵的深度优先递归算法
/// </summary>
/// <param name="gl"></param>
/// <param name="i"></param>
private void DFS(int i)
{
visited[i] = true;
//这里对顶点的操作,这里简单的打印
Debug.Log(vertexs[i]);
for (int j = 0; j < vertexs.Length; j++)
{
//edges[i, j] == 1表示两顶点是连通的
//如果是权值,可以判断: edges[i,j]!=0 && edges[i,j]! = MaxValue
//MaxValue自己定,可以用int.MaxValue,long.MaxValue,float.MaxValue等
//具体权值用int long 还是float自己定
if (edges[i, j] == 1 && !visited[j])
{
DFS(j);
}
}
}
/// <summary>
/// 邻接矩阵:广度优先遍历
/// </summary>
/// <param name="graph"></param>
public void BFSTraverse()
{
Queue<int> queue = new Queue<int>(); //初始化辅助队列
visited = new bool[maxNumVertex];
for (int i = 0; i < vertexs.Length; i++)
{
visited[i] = false;
}
for (int i = 0; i < vertexs.Length; i++) //对每一个顶点做循环
{
if (!visited[i]) //若是未访问过就处理
{
visited[i] = true; //设置当前顶点访问过
//这里对顶点的操作,这里简单的打印
Debug.Log(vertexs[i]);
queue.Enqueue(i); //将此顶点入队列
while (queue.Count > 0) //当前队列有元素
{
i = queue.Dequeue(); //出队列
for (int j = 0; j < vertexs.Length; j++)
{
//edges[i, j] == 1表示两顶点是连通的
//如果是权值,可以判断: edges[i,j]!=0 && edges[i,j]! = MaxValue
//MaxValue自己定,可以用int.MaxValue,long.MaxValue,float.MaxValue等
//具体权值用int long 还是float自己定
if (edges[i, j] == 1 && !visited[j])
{
visited[j] = true;
//这里对顶点的操作,这里简单的打印
Debug.Log(vertexs[j]);
queue.Enqueue(j);
}
}
}
}
}
}
/// <summary>
/// 普里姆算法:最小生成树
/// </summary>
public void MiniSpanTree_Prim()
{
int minWeight, i, j, minWeightIndex;
int[] adjvex = new int[vertexs.Length]; //保存相关顶点下标
int[] lowWeight = new int[vertexs.Length]; //保存相关顶点间边的权值
lowWeight[0] = 0; //初始化第一个权值为0,即V0加入生成树,lowCost的值为0,在这里就是此下标的顶点已经加入生成树
adjvex[0] = 0; //初始化第一个顶点下标为0,从顶点V0开始
//读取第一行,初始化
for (i = 1; i < vertexs.Length; i++) //循环除下标为0外的全部顶点
{
lowWeight[i] = edges[0, i]; //将V0顶点与之有边的权值存入数组
adjvex[i] = 0; //初始化都为V0的下标
}
//构造最小生成树
for (i = 1; i < vertexs.Length; i++)
{
minWeight = int.MaxValue; //初始化最小权值为极大值,通常设置为不可能的大数字,如:32765、65535等
j = 1; //顶点下标循环变量
minWeightIndex = 0; //用来存储最小权值的顶点下标
//遍历每一行的数据
while (j < vertexs.Length)
{
if (lowWeight[j] != 0 && lowWeight[j] < minWeight)
{
minWeight = lowWeight[j]; //让当前权值成为最小值
minWeightIndex = j; //将当前最小值的下标存入k
}
j++;
}
Debug.Log("打印当前顶点边中权值最小边:" + adjvex[minWeightIndex] + "---" + minWeightIndex);
lowWeight[minWeightIndex] = 0; //将当前顶点的权值设置为0,表示此顶点已经完成任务
for (j = 1; j < vertexs.Length; j++) //循环所有顶点
{
if (lowWeight[j] != 0 && edges[minWeightIndex, j] < lowWeight[j])
{
//若下标为k顶点各边权值小于此前这些顶点未被加入生成树权值
lowWeight[j] = edges[minWeightIndex, j]; //将较小权值存入lowCost
adjvex[j] = minWeightIndex; //将下标为k的顶点存入adjvex
}
}
}
}
}
边集数组:
/// <summary>
/// 边集数组
/// </summary>
public class EdgeArrary<T>
{
public class Edge : IComparable
{
public int begin;
public int end;
public int weight;
public int CompareTo(object obj)
{
Edge temp = obj as Edge;
if (weight < temp.weight)
{
return -1;
}
else if (weight > temp.weight)
{
return 1;
}
else
{
return 0;
}
}
}
private T[] vertexArr;
private List<Edge> edgeList; //采用List方便
/// <summary>
/// 创建边集数组
/// </summary>
/// <param name="vertexData">顶点数据</param>
/// <param name="edgeData">边数据</param>
public EdgeArrary(T[] vertexData, Edge[] edgeData)
{
vertexArr = vertexData;
edgeList = new List<Edge>();
edgeList.AddRange(edgeData);
}
/// <summary>
/// 创建边集数组
/// </summary>
/// <param name="vertexData">顶点数据</param>
/// <param name="edges">邻接矩阵</param>
public EdgeArrary(T[] vertexData, int[,] edges)
{
vertexArr = vertexData;
edgeList = new List<Edge>();
//把邻接矩阵转换为边集数组
for (int i = 0; i < vertexData.Length; i++)
{
for (int j = 0; j < vertexData.Length; j++)
{
if (edges[i, j] != 0 && edges[i, j] != int.MaxValue)
{
edgeList.Add(new Edge() { begin = i, end = j, weight = edges[i, j] });
}
}
}
edgeList.Sort(); //从小到大排序
}
/// <summary>
/// 克鲁斯卡尔算法:最小生成树
/// </summary>
/// <param name="graph">邻接矩阵</param>
public void MiniSpanTree_Kruskal()
{
int i, n, m;
int[] parent = new int[vertexArr.Length]; //定义一数组用来判断边与边是否形成环路
for (i = 0; i < vertexArr.Length; i++)
{
parent[i] = 0; //初始化数组值为0
}
for (i = 0; i < edgeList.Count; i++) //循环每一条边
{
n = Find(parent, edgeList[i].begin);
m = Find(parent, edgeList[i].end);
if (n != m) //假如n与m不等,说明此边没有与现有生成树形成环路
{
parent[n] = m; //将此边的结尾顶点放入下标为起点的parent中,表示此顶点已经在生成树集合中
Debug.Log("(" + edgeList[i].begin + "," + edgeList[i].end + ") " + edgeList[i].weight);
}
}
}
//查找连线顶点的尾部下标
private int Find(int[] parent, int f)
{
while (parent[f] > 0)
f = parent[f];
return f;
}
}