图的应用(最小生成树)

萝らか妹 提交于 2019-12-27 07:36:03

最小生成树

背景:假设要在n个城市之间建立通信网络,则连通n个城市只需要n-1条线路。这时。自然会考虑一个问题,如何在最节省经费的条件下建造这个通信网。在每两个城市之间都可以建立一条线路,相应的都要付出一定的经济代价。n个城市之间,最多可以设置n(n-1)/2条线路,那么,如何在这些可能的线路中选择n-1条,以使总的消耗经费最小呢?

可以用连通网表示n个城市,以及n个城市之中可能设置的线路,其中网的顶点表示城市,边表示城市之间的线路,赋予边的权值表示相应的代价。对于n各顶点连通网可以建立许多不同的生成树,每一刻生成树都可以是一个通信网。最好的通信网应该是代价之和最小的生成树。在一个连通网的所有生成树中,各边的代价之和最小的那颗树成为该连通网的最小代价生成树,简称为最小生成树

MST性质:建设N=(V,E)是一个连通网,U是顶点集V的一个非空子集。若(u,v)是一条具有最小生权值(代价)的边,其中u∈U,v∈V-U,则比存在一颗包含边(u,v)的最小生成树。

普里姆(Prim)算法和克鲁斯卡尔(Kruskal)算法是两个利用MST性质构造最小生成树的算法。

普里姆算法:

(1)普里姆算法的构造过程:
假设N=(V,E)是连通网,TE是N上最小生成树中边的集合。

  • U={u0}(u0∈V),TE={}。
  • 在所有的u∈U,v∈V-U的边(u,v)∈E中找一条权值最小的边(u0,v0)并入集合TE,同时v0并入U.
  • 重复上一步操作,直至U=V为止。
    此时TE中必有n-1条边,则T=(V,TE)为N的最小生成树。
    如图为连通网从v1出发构造最小生成树的例子。可以看出,普里姆算法逐步增加U中的顶点,可称为“加点法”。

每次选择最小边时,可能存在多条同样权值的边可选,此时任选其一即可。

普里姆算法的实现:
假设一个无向网G以邻接矩阵形式存储,从顶点u出发构造G的最小生成树T,要求输出T的各条边。为实现这个算法需要另附设一个辅助数组closedge,以记录从U到V-U具有最小权值的边。对每个顶点v1∈V-U,在辅助数组中存在一个相应分量closedge[i-1],它包括两个域:lowcost和adjvex,其中,lowcost存储最小边上的权值,adjvex存储最小变在U中的那个顶点。显然,closedge[i-1].lowcost=min{cost(u,vi)|u∈U},其中cost(u,v)表示赋予边(u,v)的权。

//辅助数组的定义,用来记录从顶点集U到V-U的权值最小的边
struct
{
    VerTexType adjvex;//最小边在U中的那个顶点
    ArcType lowcost;//最小边上的权值
}closedge[MVNum];

普里姆算法过程描述:

  1. 首先使初始顶点u加入U中,对其与每一个顶点vj,将closedge[j]均初始化到u的边信息。
  2. 循环n-1次,做以下处理:
  • 从各组边closedge中选出最小的边closedge[j],输出此边。
  • 将k加入到U中。
  • 更新剩余的每组最小边信息closedge[j],对于V-U中的边,新增加了一条从k到j的边,如果新边的权值比closedge[j].lowcost小,则将closedge[j].lowcost更新为新边的权值。

算法描述:

void MiniSpanMree_prim(AMGraph G,VexTexType u)
{
   //无向网G以邻接矩阵形式存储,从顶点u出发构造G的最小生成树T,输出T的各条边
   k=LocateVex(G,u);//k为顶点u的下标
   for(j=0;j<G.vexnum;j++)//对U中的每一个顶点vj初始化closedge[j]
       if(j!=k)  closedge[j]={u,G.arcs[k][j]};//{adjvex,lowcost}
   closedge[k].lowcost=0;//初始U={u}
   for(i=1;i<G.vexnum;i++)
   {
     //选择其余n-1个顶点,生成n-1条边(n=G.vexnum)
     k=min(closedge);
     //求出T的下一个结点:第k个顶点,closedge[k]中存有当前的最小边
     u0=closedge[k].adjvex;//u0为最小边中的一个顶点,u0∈U
     v0=G.vex[k];//v0为最小边的另一个顶点
     cout<<u0<<v0;//输出当前的最小边(u0,v0)
     closedge[k].lowcost=0;//第k个顶点并入U集(那一个顶点被推入集合,哪一个顶点的closedge置为0)
     for(j=0;j<G.vexnum;++j)
        if(G.arcs[k][j]<closedge[j].lowcost)//新顶点并入U后重新选择最小边
            closedge[j]={G.vexs[k],G.arcs[k][j]};  
   }
}

算法分析:
分许以上算法,假设网中有n个顶点,则第一次进行初始化的循环语句的频度为n,第二个循环语句的频度为n-1.其中第二个有两个内循环:其一是在closedge[v].closedge中求出最小值,其频度为n-1;其二是重新选择具有最小权值的边,其频度为n,由此,普里姆算法的时间复杂度为O(n^2),与网中的边数无关,因此适用于求稠密网的最小生成树

在这里插入图片描述
以上述图为例,从顶点v1开始构造最小生成树,给出算法中各参量的变化如下:
(注明:v1的下标i为0,第一列省略了,i=0,同时i对应的那一列对应的值全为0)
在这里插入图片描述

克鲁斯卡尔算法

(1)克鲁斯卡尔算法的构造过程

假设连通网N=(V, E),将N中的边按权值从小到大的顺序排列。

①初始状态为只有n个顶点而无边的非连通图T=(V,{}),图中每个顶点自成一个连通分量。

②在E中选择权值最小的边,若该边依附的顶点落在T中不同的连通分量上(即不形成回路),则将此边加人到T中,否则舍去此边而选择下一条权值最小的边

③重复②,直至T中所有顶点都在同一连通分量上为止。

例如,对如图所示的连通网,图示为依照克鲁斯卡尔算法构造一棵最小生成树的过程。权值分别为1、2、3、4的4条边由于满足上述条件,则先后被加入到T中,权值为5的两条边(v1, v4)和的(v3,v4)被舍去。因为它们依附的两顶点在同一连通分量上,它们若加入T中,则会使T中产生回路,而下一条权值(=5)最小的边(v2,v3) 联结两个连通分量,则可加入T,由此构造一棵最小生成树
在这里插入图片描述
可以看出,克鲁斯卡尔算法逐步增加生成树的边,与普里姆算法相比,可称为“加边法”。与普里姆算法一样, 每次选择最小边时,可能有多条同样权值的边可选,可以任选其一。

(2)克鲁斯卡尔算法的实现:

算法的实现要引人以下辅助的数据结构。
①结构体数组Edge::存储边的信息,包括边的两个顶点信息和权值。

//铺助数组Edges的定义
struct{
VerTexType Head;//边的始点
VerTexType Tail;//边的终点
ArcType lowcost;//边上的权值
}Edge[arcnum];

②Vexset[i]: 标识各个顶点所属的连通分量。对每个顶点vi∈V,在辅助数组中存在一个相应元素Vexset[i]表示该顶点所在的连通分量。初始时Vexset[i]=1,表示各顶点自成一个连通分量。

//辅助数组Vexset的定义
int Vexset [MVNum];

克鲁斯卡尔算法的算法步骤:
①将数组Edge中的元素按权值从小到大排序。
②依次查看数组Edge中的边,循环执行以下操作:
●依次从排好序的数组 Edge中选出一条边(U1,U2);
●在Vexset中分别查找v1和v2所在的连通分量vs1和vs2,进行判断:
如果vs1和vs2不等,表明所选的两个顶点分属不同的连通分量,输出此边,井合并vs1和vs2两个连通分量;
如果vs1和vs2相等,表明所选的两个顶点属于同一个连通分量,舍去此边而选择下一条权值最小的边。

算法描述:

void MiniSpanTree_Kruskal(AMGraph G)
{     //无向网G以邻接矩阵形式存储,构造G的最小生皮树T.输出T的各条边
      Sort(Edge);  //将数组Edge中的元素按权值从小到大排序
     for (i=0;i<G.vexnum;i++)  
         Vexset[i]=i;//辅助数组,表示各顶点自成一个连通分量
     for(i=0;i<G.arcnum;++i)//依次查看数组Edge中的边
   {
     v1=LocateVex(G,Edge[i}].Head);//v1为边的始点Head的下标
     v2=LocateVex(G,Edge[i].Tail);//v2为边的终点Tail的下标
     vs1=Vexset[v1];//获取边Edge[i]的始点所在的连通分量vs1
     vs2=Vexset[v2];//获取边Edge[i]的终点所在的连通分量vs2
     if(vs1 !=vs2)//边的两个顶点分属不同的连通分量
    {
     cout<< Edge[i].Head 《Edgelil.Tal://输出此边
     for(j=0;j<G.vexnum;++j)  //合井vs1和vs2两个分量,即两个集合统一编号
        if(Vexset[j]==v2)   Vexset=vs1; //集合编号为vs2的都改为vs1
        }
   }
}

[算法分析]
假若以第8章将介绍的“堆”来存放网中的边进行堆排序,对于包含e条边的网,上述算法排序时间O(elog2^e)。
在for循环中最耗时的操作是合并两个不同的连通分量,只要采取合适的数据结构,可以证明其执行时间是O(log2^e)
因此整个for循环的执行时间是O(elog2^e).
由此克鲁斯卡尔算法的时间复杂度为O(elog2^e),与网中的边数有关,与普里姆算法相比,克鲁斯卡尔算法更适用于求稀疏图的最小生成树。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!