zoukankan      html  css  js  c++  java
  • 浅谈数据结构-最小生成树

    一个连通图的生成树是一个极小的连通子图,它含有图中全部顶点,但只有足以构成一棵树的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)
    ",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("
    ");
            printf("权重数组为:");
            for(j = 0;j< pArray->numVertexes;j++)
            {
                printf("%3d ",nNodeWeight[j]);
            }
            printf("
    ");
        }
    
    }

    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
    ", k);
        printf("边集数组排序前,如下所示.
    ");  
        printf("edges[]     beign       end     weight
    ");
        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("
    ");
        }
    
    
        //下面进行排序
        InsertSort(pEdge, k);
    
        printf("边集数组排序后,如下所示.
    ");
        printf("edges[]     beign       end     weight
    ");
        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("
    ");
        }
    
    }
    //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("
    ");
    }

    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)。《此处不包括由邻接矩阵转为边集数组》, 对比两个算法,克鲁斯尔算法主要是针对边来展开,边数少时效率会非常高,所以对于稀疏图有很大的优势;而普里姆算法对于稠密图,即边数非常多的情况会更好一些。

  • 相关阅读:
    osg::BlendFunc来设置透明度
    LCA(Tarjan)
    CODEVS1073 家族 (并查集)
    CODEVS1533 互斥的数(哈希表)
    2014-12-4
    BZOJ2661 连连看 (费用流)
    2014-11-30
    JAVA语法基础作业——动手动脑以及课后实验性问题
    课后作业01——相加
    再读大道至简第二章
  • 原文地址:https://www.cnblogs.com/polly333/p/4763615.html
Copyright © 2011-2022 走看看