Prim

来源:互联网 发布:手机扫描识字软件 编辑:程序博客网 时间:2024/05/20 05:26

Prim算法

 

1.问题引入

     Prim算法(普里姆算法),图论算法的一种,用来在加权连通图中找到最小生成树。解决在一个连通网中找到一个最小生成树的问题:给定n个城市(编号由1到n),假设n个城市之间两两互相不连通。现在要在n个城市之间修路: 已知n个城市之间有m条公路(每条公路的预算经费已知)经过规划通过了省政府的审批,但为了节省资金,要求从m条公路中选出n-1条使n个城市中任意两个城市之间连通且需要的经费最少。


2,图论中的一些基本概念

     在讲解Prim算法之前需要了解一些基本的图论概念:

     2.1,

               图G(Graph)由顶点集合V(Vertex)和边集合E(Edge)两部分组成,记作G=(V,E)。通常

               用V(G),E(G)分别表示图G的顶点集合和边集合。

     2.2,无向图

               若图G中的边集合E(G)中的边均为无向边,则图G为无向图。

     2.3,子图

               有两个图G=(V,E)和G'=(V',E'),若有V'是V的子集(V'∈V)且E' 是E的子集(E'∈E),

               则称图G'为图G的子图。

     2.4,连通图

               若图G=(V,E)中任意两个顶点都连通(直接连接或间接连接),则称 G为连通图。

     2.5,权和网

               若图中的边都加上特定含义的数值,则称该图为网,称边上的数值为权值(网=图+权)。

     2.6,

               有n个顶点和n-1条边组成的无向图G,若G中任意两个顶点之间都连通(直接或间接连接)且图G中

               的边都不形成回路,则称此无向图为树。也可以简单的将树说成是没有形成回路的连通图。

     2.7,生成树

              取连通图G的n个顶点(假设图G的顶点数目为n)和n-1条边构成一个子图G',使G'满足 

              V(G')=V(G) ,E(G')∈E(G)。若G'是一个树没有形成回路的连通图),则该树为图G的

              一个生成树,也可以是说当图G去掉某些边后成为了一个树。PS:引入树的概念是因为树的性质与

              此类问题的解相一致,即任意两个顶点连通且不形成回路,不产生无用的边。

     2.8,最小生成树--Minimum Spanning Tree (MST)

                图/网(网可以理解为特殊的图--加上权值)的生成树往往不是唯一的,因此这些树中边的权值之和

                就会有所不同,将各边的权值之和最小的生成树称为图/网G的最小生成树。PS:最小生成树也不

                唯一。



3,Prim算法的基本思想

    3.1,初步辨析Prim算法与Kruskal算法

                Prim算法常被称为加点法,它通过增加目标图T(Tree)中的顶点来逐步实现最小生成树的生成。

               另外一个求解最小生成树的算法是Kruskal(克鲁斯卡尔)算法,这个算法通过增加目标图T的边

              (当边数等于n-1时就可停止遍历)实现最小生成树的生成,可以称为加边法。Prim算法因为通

               过加顶点来生成目标,因此可以用来处理边较稠密的图,其时间复杂度为O(n^2)(n为顶点数)

                ,而 Kruskal算法常被用来边数较稀疏的图,其时间复杂度为elog(e)(e为边的数目)。因为

                Kruskal 算法需要用到并查集的知识,所以需要再讲解过并查集之后再进行讲解。

     3.2,Prim算法的基本思想

             设图G=(V,E)是具有n个顶点的连通网,另外设T(P,S)(P为Peak的缩写,S为Side的缩写)

             为目标图,即最小生成树。下面是Prim算法的具体思路:

                             ,开始时V=v1,v2,v3......vn,E={e1,e2,e3......en},P(T),S(T)均为

                                       空集, 即P=Φ,S=Φ。

                               ,从V中任取一个顶点,假设所取点为v1,将v1入P中。此时V={v2,v3......vn},P={v1}。

                               ,找出E中的一个顶点在P中,另一个顶点在V中的所有边中的权值最小的一条边,并

                                       将此边的在V中的邻接点加入到P中(此时V中顶点数为n-2,P中顶点数为2)。

                               ,如果V中顶点数目为0,P中顶点数目为n,证明已经V中将n个顶点和n-1条边加入

                                       到P中,且T此时就是G的一个最小生成树。否则,则进行⑶步骤。

      3.3,prim算法的简要证明

           反证法:前提--假设prim生成的生成树不是最小生成树:

                      ⑴,设prim生成的树为T;

          ⑵,假设G'为G的最小生成树,则在G'中存在边<u,>不属于S(T);

          ⑶,将<u,v>加入T中可得一个环,且<u,v>不是该环的最长边(因为 <u,v> ∈G' ); 

          ⑷,这与prim每次生成最短边矛盾;

          ⑸,故假设不成立,命题得证。

 

4,Prim算法的实现

   4.1,几个需要解决的问题

       Prim算法的实现还有以下几个问题:

               ㈠,如何实现网G的构建:这里我们需要明白当具体实现Prim时我们需要网G

                   存所有的起点和终点以及边信息(边的权值)。

                   解决方法如下:

                   ⑴,对于起点终点的储存我们可以考虑使用邻接矩阵(二维数组),并

                       将权值赋值给各边。

                   ⑵,如果一边的两点直接相连,我们可以将此边直接赋值。对于不相邻的两点,

                       我们可以将其边初始化为无穷大(可以用0x3f3f3f3f,0x7fffffff表示无

                       穷大)。对于矩阵主对角线上的数据(也就是边起点和终点是同一个点),

                       我们可以将其赋值为0,也可以将其赋值为无穷大。

              ㈡,如何找出一个顶点在P中,另一个顶点在V中的所有边中的权值最小的一条边:

                  我们需要将目标可能存在的边一一遍历,将权值最小的边记入总数并标记,同

                  时将此边的在V中的邻接点加入到P中。

                  下面介绍两种方法:

                  ⑴,具体实现时我们可能会直接想到运用两for循环:一层是P中的顶点,一层

                      V中的顶点,同时要用两个一维数组分别记录P和V中的顶点,(假设两数组

                      为P[100],V[100]) 。从V中取点加入P中时所取点要用sign[100]数组标记。

                      (事实上因为在本文中的问题上因为V中数据是1--n,是连续的,因此不使用V

                      数组也可以)这样,当P中元素个数为n时表明已经找出最小生成树。

                      如下表:(代码随后附上吐舌头)

                              P                       V                                 sign第一次P[1]=1V[1]=1,V[2]=2,V[3]=3,V[4]=4,V[5]=5sign[1]=1第二次P[1]=1,P[2]=5V[1]=1,V[2]=2,V[3]=3,V[4]=4,V[5]=5sign[1]=1,sign[5]=1第三次P[1]=1,P[2]=5,P[3]=3V[1]=1,V[2]=2,V[3]=3,V[4]=4,V[5]=5sign[1]=1,sign[3]=1,sign[5]=1第四次P[1]=1,P[2]=5,P[3]=3,P[4]=2
V[1]=1,V[2]=2,V[3]=3,V[4]=4,V[5]=5
sign[1]=1,sign[2]=1,sign[3]=1,sign[5]=1第五次P[1]=1,P[2]=5,P[3]=3,P[4]=2,P[5]=4V[1]=1,V[2]=2,V[3]=3,V[4]=4,V[5]=5sign[1]=1,sign[2]=1,sign[3]=1,sign[4]=1,
sign[5]=1
                                                                      

                  ⑵,第⑴种方法对于理解Prim算法的基本思想非常有用,但实际应用中要用到3

                      重for循环,时间复杂度较高。第⑵种方法理解起来有些困难,但却是最实

                      用的一种方法:奋斗

                      这里要用到一个一维数组dist[100]用来存储P中的点到V中的点的权值,仍  

                      然用sign[100]标记加入到P中的点。刚开始将定点1加入P,则P={1},

                      V={2,3,......n},sign[1]=1。(在这里不再使用P,V数组,而仅用sign数

                      组进行标记)

dist初始化如下:

① sign[1]=1;for(i=1;i<=n;i++){dist[i]=graph[1][i];//将1到其他定点的权值赋值给dist}


 

接下来要找出dist数组(下标1--n)中权值最小值并标记:

 

②for(j=1;j<=n;j++)  {      min=INF;      if(!sign[j]&&dist[j]<min)//找出V中一点,用u记录,dist[u]为dist数组中的最小值       {          min=dist[j];          u=j;      }  }  s+=min;//记录最小生成树的总权值   sign[u]=1;//标记u,就表示将u加入P中


                                                                                          

在②中找到了最小生成树的一个节点(1到其它节点的最小值),但接下来的问题是:如何更新dist数组使dist记录的是:一个顶点在P中,另一顶点在V中的所有边的权值。

③for(j=1;j<=n;j++){if(!sign[j]&&graph[u][j]<dist[j]){dist[j]=graph[u][j];}}

                                                                                         

通过③的代码我们可以看到其实dist不需要记录一个顶点在P中,另一顶点在V中的所有边的权值,只需要更新时保证一个顶点在P中,另一顶点在V中的所有边中的权值最小的一条边记录在dist数组中就可以了。在这里通过新加入最小生成树的节点u来更新P中的点到V中的点的权值,使dist[j]记录的是P中的所有点到j(V中的一点)的权值中的最小值,所以下一个最小生成树权值一定包含在dist数组中,且此时dist数组的下标就是最小生成树的一个节点

PS:对于上面的代码只需要将②③包含在同一个for循环中就可以找出另外n-1个节点。

 

4.2,代码附上

     ㈠,基于4.1㈡①的代码实现:

 

<pre class="cpp" name="code">#include<cstdio>#include<cstring>#define INF 0x3f3f3f3fint graph[100][100];int P[100];//P记录最小生成树的顶点bool sign[100];//标记加入P中的顶点 void prim(int n){int k=1;int u=1;int sum=0;//记录最小生成树的总权值(修建公路的总花费) memset(sign,false,sizeof(sign));P[1]=1;//P中有一个顶点(此顶点可任选),V中有n-1个顶点 sign[1]=true;for(int i=1;i<n;i++)//在往P中加入n-1个顶点 {int min=INF;for(int j=1;j<=k;j++)//两重for循环,找出P中的点到V中的点的最短路径 {for(int h=1;h<=n;h++){if(!sign[h]&&graph[P[j]][h]<min){min=graph[P[j]][h];u=h;}}} sign[u]=true;P[++k]=u;sum+=min;}printf("最小生成树路径:");for(int i=1;i<=k;i++){printf("%d%s",P[i],i==k?"\n":"-->"); } printf("最小生成树总权值:%d\n",sum);}int main(){int n,m;//n个城市,m条边公路 int u,v,w;//u,v两个城市相连且花费为w while(~scanf("%d%d",&n,&m)){memset(graph,INF,sizeof(graph));//graph初始化为INF for(int i=0;i<m;i++){scanf("%d%d%d",&u,&v,&w);graph[u][v]=graph[v][u]=w;//graph所表示的为无向图 }prim(n); } return 0;}

     

     ㈡,基于4.1㈡②的代码实现:

 

#include<cstdio>#include<cstring>#define INF 0x3f3f3f3fint graph[100][100];int P[100];bool sign[100];//标记加入P中的顶点 int dist[100];void prim(int n){memset(sign,false,sizeof(sign));int sum=0;//记录最小生成树的总权值(修建公路的总花费) int u=1,k=1;P[1]=1;//P中有一个顶点(此顶点可任选),V中有n-1个顶点 sign[1]=true; for(int i=1;i<=n;i++)//初始化 {dist[i]=graph[1][i];//将1到其他定点的权值赋值给dist}for(int i=1;i<n;i++)//在往P中加入n-1个顶点 {int min=INF;        for(int j=1;j<=n;j++)  {       if(!sign[j]&&dist[j]<min)//找出V中一点,用u记录,dist[u]为dist数组中的最小值       {          min=dist[j];          u=j;      }  } sign[u]=true;P[++k]=u;sum+=min;for(int j=1;j<=n;j++)//更新dist数组 {if(!sign[j]&&graph[u][j]<dist[j]){dist[j]=graph[u][j];}}}printf("最小生成树路径:");for(int i=1;i<=k;i++){printf("%d%s",P[i],i==k?"\n":"-->"); } printf("最小生成树总权值:%d\n",sum);}int main(){int n,m;//n个城市,m条边公路 int u,v,w;//u,v两个城市相连且花费为w while(~scanf("%d%d",&n,&m)){memset(graph,INF,sizeof(graph));//graph初始化为INF for(int i=0;i<m;i++){scanf("%d%d%d",&u,&v,&w);graph[u][v]=graph[v][u]=w;//graph所表示的为无向图 }prim(n); } return 0;}

 

     ㈢,测试实例及结果

根据上图写出测试数据:

6 103 1 13 2 53 4 53 5 63 6 41 2 62 5 35 6 66 4 24 1 5

测试结果(两部分程序结果一致):     

 

本文参考:

            http://wk.baidu.com/view/bd0300d2b14e852458fb57d2?pcf=2

            http://www.cnblogs.com/biyeymyhjob/archive/2012/07/30/2615542.html

            http://blog.sina.com.cn/s/blog_81ca6e750101gw4n.html          

             

本文写的有许多不足之处,欢迎大家批评指正微笑
                         










                      

    


                      


0 0
原创粉丝点击