/*--------------------CSS部分-------------------*/ /*--------------------JS部分-------------------*/

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

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

一、普利姆(Prim)算法

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

1、算法思想

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

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

2、算法分析

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

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

}

 

image

image

5、程序分析

  1. 首先选取A作为初始顶点,从边的邻接矩阵中得知,最近的点是D,坐标是3,边表示为(0,3)
  2. 这时候权重数组中坐标为3的设为0.
  3. 在所有顶点中寻找到D的最小距离的顶点,从邻接矩阵得到是坐标为5,就是顶点F,边表示为(3,5)
  4. 将D中一行的权重与权重数组比较,将较小的值,保存其中。
  5. 在权重数组中寻找最小的且不为0的,发现时权重为6,坐标是5,就是之前确定的边(3,5),已F开始需找到F的最小边。如此循环。
  6. 从坐标数组中我们得知边为(nNodeInde[i],i),所以为(0,1)(4,2)(0,3)(1,4)(3,5)(4,6)。

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

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

1、算法思想

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

  1. 将所有的边按照权重非递减排序;
  2. 选择最小权重的边,判断是否其在当前的生成树中形成了一个环路。如果环路没有形成,则将该边加入树中,否则放弃。
  3. 重复步骤 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[n] = m;  //将此边的结尾顶点放入下标为起点的parent中
            //表示此顶点已经在生成树集合中
            printf("(%d,%d) %d ", edges[i].begin, edges[i].end, edges[i].weight);
        }
    }
    printf("\n");
}

image

5、代码分析

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

  1. 在开始(4,5)边的权值最小,在parent中parent[4]与parent[5],都为0,所以返回4,5,两者不相等,此时将parent[4] = 5,此时说明4,5之间有联系了。
  2. 同样是(2,8),此时parent[2] = 8;
  3. 继续循环,parent[0] = 1;关键是是边3是应该是(0,5),(之前是parent中0已经有值,继续判断为,此时parent[0] = 1),此时parent[1] = 5.。
  4. 同样继续循环将图进行输出,填满parent。
  5. 括号中就是最小生成树的边。

三、总结

克鲁斯卡尔算法的Find函数由边数e决定,时间复杂度为O(loge),而外面有一个for循环e次,所以克鲁斯卡尔算法的时间复杂度为O(eloge)。《此处不包括由邻接矩阵转为边集数组》, 对比两个算法,克鲁斯尔算法主要是针对边来展开,边数少时效率会非常高,所以对于稀疏图有很大的优势;而普里姆算法对于稠密图,即边数非常多的情况会更好一些。

posted @ 2015-08-27 16:03  bldong  阅读(1034)  评论(0编辑  收藏  举报