最小生成树——普里姆(Prim)算法详解和实现

最小生成树

  • 普里姆(Prim)算法
    • 代码实现
    • 效率分析
    • 思考

普里姆(Prim)算法

  1. 普里姆算法的基本思想是:假设连通网络为 N=(V,E);
    TE为N的最小生成树上边的集合,开始时TE为空集;
    U为算法在构造最小生成树过程中已得到的顶点集,开始时U={ u0 }(u0属于V)。
    (1) 算法从N中的某一顶点u0出发,选择与u0关联的具有最小权值的边(u0,vi),将顶点vi加入到生成树的顶点集合U中,(u0,vi)加入到集合TE中;
    (2) 以后每一步从一个顶点在U中,而另一个顶点在(V-U)中的各条边当中选择权值最小的边(u,v)(u属于U,v属于V-U),把顶点v加入到集合U中,边(v,u)加入到集合TE中。
    (3) 如此重复,直到网络中的所有顶点都加入到生成树顶点集合U(U=V)中为止。此时,TE中刚好有n-1条边,则T=(V,TE)为N的最小生成树。
  2. 例如,对于图7-18(a)所示的连通网络,图7-2l(a)~(f)给出了按普里姆算法构造最小生成树的过程。
    最小生成树——普里姆(Prim)算法详解和实现_第1张图片
  3. 在利用普里姆算法构造最小生成树过程中,需要设置一个辅助数组closearc[ ],以记录从V-U中顶点到U中顶点具有最小权值的边。对每一个顶点v属于V-U,在辅助数组中有一个分量closearc[v],它包括两个域:lowweight 和nearvertex。其中,lowweight中存放顶点v到U中的各顶点的边上的当前最小权值(lowweight=0表示v属于U);nearvertex记录顶点v到U中具有最小权值的那条边的另一个邻接顶点u(nearvertex=-1表示该顶点v为开始顶点)。
    也就是说,通过 lowweight=0和 lowweight != 0将顶点分为已经在生成树中和还未包含入生成树中;用nearvertex记录到达当前节点的前一个结点,而因 为每次取权值最小,从nearvertex = -1到当前结点的nearvertex可以得到从第一个结点到当前结点的最短路径。
  4. 在下面的普里姆算法描述中,连通网络采用邻接矩阵作为存储结构,并假设普里姆算法从顶点A(设顶点A的序号为0)出发(即u0=0)。普里姆算法步骤如下:
    1)初始化辅助数组closearc[]。
    2)重复下列步骤3)和4)n-1次。
    3)在closearc[]中选择lowweight≠0&&lowweight最小的顶点v,即选中的权值最小的边为(closearc[v].nearvertex,i)。将closearc[v].lowweight改为0,表示顶点i已加入顶点集U中,并将边(closearc[v].nearvertex,v)加入生成树T的边集合。
    4)对V-U中的每一个顶点 j,如果依附于顶点 j 和刚加入U集合的新顶点 v 的边的权值Arcs[ v ][ j ]小于原来依附于 j 和生成树顶点集合中顶点的边的最短距离
    closearc[ j ].lowweight,则修改 closcarc[ j ],使其lowweight=arcs[ v ][ j ],nearvertex=v。
    最小生成树——普里姆(Prim)算法详解和实现_第2张图片

代码实现

#include "AdjMatrixUndirGraph.h"
template<class ElemType,class WeightType> struct CloseArcType
{
     
    WeightType lowweight;
    int nearvertex;
};
template<class ElemType,class WeightType> void
MinSpanTreePrim(const AdjMatrixUndirGraph<ElemType,WeightType> &g,int u0)
{
     
    WeightType min;
    ElemType v1,v2;
    int vexnum=g.GetVexNum();
    CloseArcType<ElemType,WeightType> *closearc;
    if(u0<0||u0>=vexnum)
        throw Error("顶点u0不存在!");
    int u,v,k;
    closearc=new CloseArcType<ElemType,WeightType>[vexnum];
    for(v=0; v<vexnum; v++)
    {
     
        //初始化辅助数组adjVex,并对顶点作标志,此时U={v0}
        closearc[v].nearvertex=u0;
        closearc[v].lowweight=g.GetWeight(u0,v);
    }
    closearc[u0].nearvertex=-1;
    closearc[u0].lowweight=0;

    for(k=1; k<vexnum; k++)
    {
     
        //选择生成树的其余net.GetVexNum()-1 个顶点
        min=g.GetInfinity();
        v=u0;//选择使得边为连接V-U到U的具有最小权值的边
        for(u=0; u<vexnum; u++)
            if(closearc[u].lowweight!=0 && closearc[u].lowweight<min)//找到最小权值
            {
     
                v=u;
                min=closearc[u].lowweight;
            }

        if(v!=u0)
        {
     
            g.GetElem(closearc[v].nearvertex,v1);
            g.GetElem(v,v2);
            cout<<"edge:( "<<v1<<" , "<<v2<<" ) weight:"<<min<<endl;
            closearc[v].lowweight=0;//将w并入U
            for(u=g.FirstAdjVex(v); u!=-1; u=g.NextAdjVex(v,u))
                //新顶点并入U后重新选择最小边
                if(closearc[u].lowweight!=0 && (g.GetWeight(v,u)<closearc[u].lowweight))
                {
     
                    closearc[u].lowweight=g.GetWeight(v,u);
                    closearc[u].nearvertex=v;
                }
        }
    }
    delete [] closearc;
}

效率分析

在普里姆算法中,主要有两个并列的for 循环。
设连通网络由n个顶点和e条边组成,则第一个for循环执行n-1次对辅助数组closearc[]进行初始化;第二个for循环是个二重嵌套循环,外循环执行n-1次把n-1个顶点加入U中,对每加入一个顶点到U,有两个并列的for循环分别实现查找具有最小权值的边和修改辅助数组closearc[],它们的时间复杂度均为O(n),所以整个算法的间复杂度为O(n^2)。
由此可见,普里姆算法适用于边稠密的连通网络

思考

  1. Q:如何记录顶点已经被选中?
    A:通过构造辅助数组lowweight以记录已生成的结点集同剩下的各个未被选中的结点之间的最短权值。若结点已经被选中,则设置对应序号的lowweight数组元素值为0。
  2. Q:如何记录信息以便于知道加入一个结点后哪条边的权值最小?
    A:通过找出lowweight数组中的最小值。
  3. Q:如何将权值最小的边连接的未被选中的结点加入最小生成树?
    A:通过构造辅助数组nearVertex以记录每一个被选中结点的相邻结点,再将未被选中的结点加入最小生成树时,修改对应序号的lowweight数组元素值为0,修改对应序号的nearVertex数组值为其相邻结点。

你可能感兴趣的:(#,数据结构,数据结构)