您的位置:首页 > 理论基础 > 数据结构算法

浅谈数据结构-最小生成树

2015-08-27 16:03 447 查看
一个连通图的生成树是一个极小的连通子图,它含有图中全部顶点,但只有足以构成一棵树的n-1条边。那么我们把构造连通网的最小代价生成树称为最小生成树。 找连通网的最小生成树,经典的有两种算法,普里姆算法和克鲁斯卡尔算法。

一、普利姆(Prim)算法

普利姆算法,图论中一种算法,可在加权连通图里搜索最小生成树。此算法搜索到的边子集所构成的树中,不但包括连通图里的所有顶点,且所有边的权值最小。

1、算法思想

从单一顶点开始,普利姆算法按照以下步骤逐步扩大树中所包含顶点的数目,直到遍及连通图的所有顶点。

输入:一个加权连通图,含有顶点V,边集合为E;
初始化:确定连通图的初始点,Vnew = {x},Enew = 0;
循环:直到Vnew = V 在集合E中选取权值最小的边<u, v>,其中u为集合Vnew中的元素,而v不在Vnew集合当中,并且v∈V(如果存在有多条满足前述条件即具有相同权值的边,则可任意选取其中之一);
将v加入集合Vnew中,将<u, v>边加入集合Enew中;

输出:使用集合Vnew和Enew来描述所得到的最小生成树。

2、算法分析

根据算法思想,整理下程序设计需要

创建一个图的存储结构(文章是邻接矩阵)
创建一个数组,保存图中的点(其实是在邻接矩阵中顶点表的坐标),其中全部初始化为0.
创建一个数组,保存边集合中的权重,保存顶点之间的权值。假如从D顶点开始建立树结构,这个数组就是表示D到各个顶点的距离。 权重为0.表示次顶点完成任务。
在权重数组中找出与初始顶点的权重最小的顶点,记录下的坐标。 并将权重数组中权重设为0.

3、图例解释





4、示例代码

//prime算法
void GraphData::MiniSpanTree_Prime(GraphArray *pArray)
{
int min,i,j,k;
int nNodeIndex[MAXVEX];      //保存相关顶点坐标,1就是已经遍历访问的过结点
int nNodeWeight[MAXVEX];     //保存某个顶点到各个顶点的权值,为不为0和最大值表示遍历过了。
//两个数组的初始化
printf("开始初始化,当前顶点边的权值为:");
for(i = 0;i<pArray->numVertexes;i++)
{
nNodeIndex[i] = 0;
nNodeWeight[i] = pArray->arg[0][i];//设定在矩阵中第一个顶点为初始点。
printf(" %c",nNodeWeight[i]);
}
//Prime算法思想
for (i = 1;i< pArray->numVertexes;i++)
{
min = INFINITY;    //初始化权值为最大值;
j = 1;
k = 0;
// 循环全部顶点,寻找与初始点边权值最小的顶点,记下权值和坐标
while(j < pArray->numVertexes)
{
//如果权值不为0,且权值小于min,为0表示本身
if (nNodeWeight[j] != 0&&nNodeWeight[j] < min)
{
min     = nNodeWeight[j];
k = j;     //保存上述顶点的坐标值
}
j++;
}
printf("当前顶点边中权值最小边(%d,%d)\n",nNodeIndex[k] , k); //打印当前顶点边中权值最小
nNodeWeight[k] = 0; //将当前顶点的权值设置为0,表示此顶点已经完成任务

for (j = 1;j< pArray->numVertexes;j++)  //循环所有顶点,查找与k顶点的最小边
{
//若下标为k的顶点各边权值小于此前这些顶点未被加入的生成树权值
if (nNodeWeight[j] != 0&&pArray->arg[k][j] < nNodeWeight[j])
{
nNodeWeight[j] = pArray->arg[k][j];
nNodeIndex[j] = k;     //将下标为k的顶点存入adjvex
}
}
//打印当前顶点状况
printf("坐标点数组为:");
for(j = 0;j< pArray->numVertexes;j++)
{
printf("%3d ",nNodeIndex[j]);
}
printf("\n");
printf("权重数组为:");
for(j = 0;j< pArray->numVertexes;j++)
{
printf("%3d ",nNodeWeight[j]);
}
printf("\n");
}

}


 









5、程序分析

首先选取A作为初始顶点,从边的邻接矩阵中得知,最近的点是D,坐标是3,边表示为(0,3)

这时候权重数组中坐标为3的设为0.

在所有顶点中寻找到D的最小距离的顶点,从邻接矩阵得到是坐标为5,就是顶点F,边表示为(3,5)

将D中一行的权重与权重数组比较,将较小的值,保存其中。

在权重数组中寻找最小的且不为0的,发现时权重为6,坐标是5,就是之前确定的边(3,5),已F开始需找到F的最小边。如此循环。

从坐标数组中我们得知边为(nNodeInde[i],i),所以为(0,1)(4,2)(0,3)(1,4)(3,5)(4,6)。

二、克鲁斯卡尔(Kruskal)算法

普利姆算法是从某一个顶点开始,逐步找各个顶点上最小权值的边构建来最小生成树。同样的思路,我们用边来构建生成树,同时在构建时,需要考虑是否会生成环路.

1、算法思想

Kruskal 算法提供一种在 O(ElogV) 运行时间确定最小生成树的方案。Kruskal 算法基于贪心算法(Greedy Algorithm)的思想进行设计,其选择的贪心策略就是,每次都选择权重最小的但未形成环路的边加入到生成树中。其算法结构如下:

将所有的边按照权重非递减排序;

选择最小权重的边,判断是否其在当前的生成树中形成了一个环路。如果环路没有形成,则将该边加入树中,否则放弃。

重复步骤 2,直到有 V – 1 条边在生成树中。

 

2、算法分析

Kruskal 算法是以分析边为基础,则需要建立边集数组结构,也就是在程序中需要将邻接矩阵转化为边集数组。

//对边集数组Edge结构的定义
typedef struct
{
int begin;
int end;
int weight;
}Edge;


程序将邻接矩阵通过程序转化为边集数组,并且对它们的按权值从小到大排序.



3、图例解释


首先第一步,我们有一张图Graph,有若干点和边



将所有的边的长度排序,用排序的结果作为我们选择边的依据。这里再次体现了贪心算法的思想。资源排序,对局部最优的资源进行选择,排序完成后,我们率先选择了边AD。这样我们的图就变成了右图


在剩下的变中寻找。我们找到了CE。这里边的权重也是5


依次类推我们找到了6,7,7,即DF,AB,BE。



下面继续选择, BC或者EF尽管现在长度为8的边是最小的未选择的边。但是现在他们已经连通了(对于BC可以通过CE,EB来连接,类似的EF可以通过EB,BA,AD,DF来接连)。所以不需要选择他们。类似的BD也已经连通了(这里上图的连通线用红色表示了)。

最后就剩下EG和FG了。当然我们选择了EG。最后成功的图就是上图了。

4、代码

//查找连线顶点尾部
int GraphData::FindLastLine(int *parent,int f)
{
while(parent[f] >0)
{
f = parent[f];
}
return f;
}
//直接插入排序
void GraphData::InsertSort(Edge *pEdge,int k)
{
Edge *itemEdge = pEdge;
Edge item;
int i,j;
for (i = 1;i<k;i++)
{
if (itemEdge[i].weight < itemEdge[i-1].weight)
{
item = itemEdge[i];
for (j = i -1; itemEdge[j].weight > item.weight ;j--)
{
itemEdge[j+1] = itemEdge[j];
}
itemEdge[j+1] = item;
}
}
}
//将邻接矩阵转化为边集数组
void GraphData::GraphToEdges(GraphArray *pArray,Edge *pEdge)
{
int i;
int j;
int k;

k = 0;
for(i = 0; i < pArray->numVertexes; i++)
{
for(j = i; j < pArray->numEdges; j++)
{
if(pArray->arg[i][j] < 65535)
{
pEdge[k].begin = i;
pEdge[k].end = j;
pEdge[k].weight = pArray->arg[i][j];
k++;
}
}
}

printf("k = %d\n", k);
printf("边集数组排序前,如下所示.\n");
printf("edges[]     beign       end     weight\n");
for(i = 0; i < k; i++)
{
printf("%d", i);
printf("        %d", pEdge[i].begin);
printf("        %d", pEdge[i].end);
printf("        %d", pEdge[i].weight);
printf("\n");
}

//下面进行排序
InsertSort(pEdge, k);

printf("边集数组排序后,如下所示.\n");
printf("edges[]     beign       end     weight\n");
for(i = 0; i < k; i++)
{
printf("%d", i);
printf("        %d", pEdge[i].begin);
printf("        %d", pEdge[i].end);
printf("        %d", pEdge[i].weight);
printf("\n");
}

}
//Kruskal算法(克鲁斯卡尔)
void GraphData::MiniSpanTree_Kruskal(GraphArray *pArray)
{
int i,n,m;
int parent[MAXVEX];    //定义边集数组
Edge edges[MAXVEX];    //定义一数组用来判断边与边是否形成环
//邻接矩阵转为边集数组,并按照权值大小排序
GraphToEdges(pArray,edges);

for (i =0; i< pArray->numVertexes;i++)
{
parent[i] = 0;    //初始化数组数值为0

}

//算法关键实现
for (i = 0;i < pArray->numVertexes;i++)    //循环每条边
{
//根据边集数组,查找出不为0的边
n = FindLastLine(parent,edges[i].begin);
m = FindLastLine(parent,edges[i].end);
printf("边%d的开始序号为:%d,结束为:%d)",i,n,m);
if(n != m)      //假如n与m不等,说明此边没有与现有生成树形成环路
{
parent
= m;  //将此边的结尾顶点放入下标为起点的parent中
//表示此顶点已经在生成树集合中
printf("(%d,%d) %d ", edges[i].begin, edges[i].end, edges[i].weight);
}
}
printf("\n");
}






5、代码分析

在输入14个点位后,根据权值排序得到上图,上图表示边集数组的排序后的结果。

在开始(4,5)边的权值最小,在parent中parent[4]与parent[5],都为0,所以返回4,5,两者不相等,此时将parent[4] = 5,此时说明4,5之间有联系了。

同样是(2,8),此时parent[2] = 8;

继续循环,parent[0] = 1;关键是是边3是应该是(0,5),(之前是parent中0已经有值,继续判断为,此时parent[0] = 1),此时parent[1] = 5.。

同样继续循环将图进行输出,填满parent。

括号中就是最小生成树的边。

三、总结

克鲁斯卡尔算法的Find函数由边数e决定,时间复杂度为O(loge),而外面有一个for循环e次,所以克鲁斯卡尔算法的时间复杂度为O(eloge)。《此处不包括由邻接矩阵转为边集数组》, 对比两个算法,克鲁斯尔算法主要是针对边来展开,边数少时效率会非常高,所以对于稀疏图有很大的优势;而普里姆算法对于稠密图,即边数非常多的情况会更好一些。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: