数据结构 第16讲 沟通无限校园网——最小生成树(prim算法)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/rainchxy/article/details/78779429

本内容来源于本人著作《趣学算法》,在线章节:http://www.epubit.com.cn/book/details/4825

校园网是为学校师生提供资源共享、信息交流和协同工作的计算机网络。校园网是一个宽带、具有交互功能和专业性很强的局域网络。如果一所学校包括多个学院及部门,也可以形成多个局域网络,并通过有线或无线方式连接起来。原来的网络系统只局限于以学院、图书馆为单位的局域网,不能形成集中管理以及各种资源的共享,个别学院还远离大学本部,这些情况严重地阻碍了整个学校的网络化需求。现在需要设计网络电缆布线,将各个单位的局域网络连通起来,如何设计能够使费用最少呢?

图2-58 校园网络

2.7.1 问题分析

某学校下设10个学院,3个研究所,1个大型图书馆,4个实验室。其中,1~10号节点代表10个学院,11~13号节点代表3个研究所,14号节点代表图书馆,15~18号节点代表4个实验室。该问题用无向连通图=(VE)来表示通信网络,V表示顶点集,E表示边集。把各个单位抽象为图中的顶点,顶点与顶点之间的边表示单位之间的通信网络,边的权值表示布线的费用。如果两个节点之间没有连线,代表这两个单位之间不能布线,费用为无穷大。如图2-59所示。

图2-59 校园网连通图

那么我们如何设计网络电缆布线,将各个单位连通起来,并且费用最少呢?

对于n个顶点的连通图,只需n−1条边就可以使这个图连通,n−1条边要想保证图连通,就必须不含回路,所以我们只需要找出n−1条权值最小且无回路的边即可。

需要说明几个概念。

(1)子图:从原图中选中一些顶点和边组成的图,称为原图的子图。

(2)生成子图:选中一些边和所有顶点组成的图,称为原图的生成子图。

(3)生成树:如果生成子图恰好是一棵树,则称为生成树。

(4)最小生成树:权值之和最小的生成树,则称为最小生成树。

本题就是最小生成树求解问题。

2.7.2 算法设计

找出n−1条权值最小的边很容易,那么怎么保证无回路呢?

如果在一个图中深度搜索或广度搜索有没有回路,是一件繁重的工作。有一个很好的办法——避圈法。在生成树的过程中,我们把已经在生成树中的结点看作一个集合,把剩下的结点看作另一个集合,从连接两个集合的边中选择一条权值最小的边即可。

首先任选一个结点,例如1号结点,把它放在集合U中,U={1},那么剩下的结点即VU={2,3,4,5,6,7},V是图的所有顶点集合。如图2-60所示。

..\17-0245 图\0268.tif

图2-60 最小生成树求解过程

现在只需在连接两个集合(VVU)的边中看哪一条边权值最小,把权值最小的边关联的结点加入到集合U。从图2-68可以看出,连接两个集合的3条边中,结点1到结点2的边权值最小,选中此条边,把2号结点加入U集合U={1,2},VU={3,4,5,6,7}。

再从连接两个集合(VVU)的边中选择一条权值最小的边。从图2-61可以看出,连接两个集合的4条边中,结点2到结点7的边权值最小,选中此条边,把7号结点加入U集合U={1,2,7},VU={3,4,5,6}。

图2-61 最小生成树求解过程

如此下去,直到U=V结束,选中的边和所有的结点组成的图就是最小生成树。

是不是非常简单啊?

这就是Prim算法,1957年由美国计算机科学家Robert C.Prim发现的。那么如何用算法来实现呢?

首先,令U={u0},u0VTE={}。u0可以是任何一个结点,因为最小生成树包含所有结点,所以从哪个结点出发都可以得到最小生成树,不影响最终结果。TE为选中的边集。

然后,做如下贪心选择:选取连接UVU的所有边中的最短边,即满足条件iUjVU,且边(ij)是连接UVU的所有边中的最短边,即该边的权值最小。

然后,将顶点j加入集合U,边(ij)加入TE。继续上面的贪心选择一直进行到U=V为止,此时,选取到的所有边恰好构成图G的一棵最小生成树T

算法设计及步骤如下。

步骤1:确定合适的数据结构。设置带权邻接矩阵C存储图G,如果图G中存在边(ux),令C[u][x]等于边(ux)上的权值,否则,C[u][x]=∞;bool数组s[],如果s[i]=true,说明顶点i已加入集合U

如图2-62所示,直观地看图很容易找出 集合到 VU集合的边中哪条边是最小的,但是程序中如果穷举这些边,再找最小值就太麻烦了,那怎么办呢?

..\17-0245 图\0270.tif

图2-62 最小生成树求解过程

可以通过设置两个数组巧妙地解决这个问题,closest[j]表示VU中的顶点j到集合U中的最邻近点,lowcost[j]表示VU中的顶点j到集合U中的最邻近点的边值,即边(j,closest[j])的权值。

例如,在图2-62中,7号结点到U集合中的最邻近点是2,closest[7]=2,如图2-63所示。7号结点到最邻近点2的边值为1,即边(2,7)的权值,记为lowcost[7]=1,如图2-64所示。

图2-63 closest[]数组

图2-64 lowcost[]数组

只需要在VU集合中找lowcost[]值最小的顶点即可。

步骤2:初始化。令集合U={u0},u0V,并初始化数组closest[]、lowcost[]和s[]。

步骤3:在VU集合中找lowcost值最小的顶点t,即lowcost[t]=min{lowcost[j]|jVU},满足该公式的顶点t就是集合VU中连接集合U的最邻近点。

步骤4:将顶点t加入集合U

步骤5:如果集合VU为空,算法结束,否则,转步骤6。

步骤6:对集合VU中的所有顶点j,更新其lowcost[]和closest[]。更新公式:if(C[t] [j]<lowcost [j] ) { lowcost [j]= C [t] [j]; closest [j] = t; },转步骤3。

按照上述步骤,最终可以得到一棵权值之和最小的生成树。

2.7.3 完美图解

=(VE)是无向连通带权图,如图2-65所示。

图2-65 无向连通带权图G

(1)数据结构

设置地图的带权邻接矩阵为C[][],即如果从顶点i到顶点j有边,就让C[i][j]=<ij>的权值,否则C[i][j]=∞(无穷大),如图2-66所示。

图2-66 邻接矩阵C[ ][ ]

(2)初始化

假设u0=1;令集合U={1},VU={2,3,4,5,6,7},TE={},s[1]=true,初始化数组closest[]:除了1号结点外其余结点均为1,表示VU中的顶点到集合U的最临近点均为1,如图2-67所示。lowcost[]:1号结点到VU中的顶点的边值,即读取邻接矩阵第1行,如图2-68所示。

图2-67 closest[]数组

图2-68 lowcost[]数组

初始化后如图2-69所示。

..\17-0245 图\0277.tif

图2-69 最小生成树求解过程

(3)找最小

在集合VU={2,3,4,5,6,7}中,依照贪心策略寻找VU集合中lowcost最小的顶点t,如图2-70所示。

图2-70 lowcost[]数组

找到最小值为23,对应的结点t=2。

选中的边和结点如图2-71所示。

图2-71 最小生成树求解过程

(4)加入U战队

将顶点t加入集合U={1,2},同时更新VU={3,4,5,6,7}。

(5)更新

刚刚找到了到U集合的最邻近点= 2,那么对t在集合VU中每一个邻接点j,都可以借助t更新。我们从图或邻接矩阵可以看出,2号结点的邻接点是3和7号结点:

C[2][3]=20<lowcost[3]=∞,更新最邻近距离lowcost[3]=20,最邻近点closest[3]=2;

C[2][7]=1<lowcost[7]=36,更新最邻近距离lowcost[7]=1,最邻近点closest[7]=2;

更新后的closest[j]和lowcost[j]数组如图2-72和图2-73所示。

图2-72 closest[]数组

图2-73 lowcost[]数组

更新后如图2-74所示。

..\17-0245 图\0282.tif

图2-74 最小生成树求解过程

closest[j]和lowcost[j]分别表示VU集合中顶点jU集合的最邻近顶点和最邻近距离。3号顶点到U集合的最邻近点为2,最邻近距离为20;4、5号顶点到U集合的最邻近点仍为初始化状态1,最邻近距离为∞;6号顶点到U集合的最邻近点为1,最邻近距离为28;7号顶点到U集合的最邻近点为2,最邻近距离为1。

(6)找最小

在集合VU={3,4,5,6,7}中,依照贪心策略寻找VU集合中lowcost最小的顶点t,如图2-75所示。

..\17-0245 图\0283.tif

图2-75 lowcost[]数组

找到最小值为1,对应的结点t=7。

选中的边和结点如图2-76所示。

图2-76 最小生成树求解过程

(7)加入U战队

将顶点t加入集合U={1,2,7},同时更新VU={3,4,5,6}。

(8)更新

刚刚找到了到U集合的最邻近点t =7,那么对t在集合VU中每一个邻接点j,都可以借t更新。我们从图或邻接矩阵可以看出,7号结点在集合VU中的邻接点是3、4、5、6结点:

C[7][3]=4<lowcost[3]=20,更新最邻近距离lowcost[3]=4,最邻近点closest[3]=7;

C[7][4]=9<lowcost[4]=∞,更新最邻近距离lowcost[4]=9,最邻近点closest[4]=7;

C[7][5]=16<lowcost[5]=∞,更新最邻近距离lowcost[5]=16,最邻近点closest[5]=7;

C[7][6]=25<lowcost[6]=28,更新最邻近距离lowcost[6]=25,最邻近点closest[6]=7;

更新后的closest[j]和lowcost[j]数组如图2-77和图2-78所示。

图2-77 closest[]数组

图2-78 lowcost[]数组

更新后如图2-79所示。

..\17-0245 图\0287.tif

图2-79 最小生成树求解过程

closest[j]和lowcost[j]分别表示VU集合中顶点jU集合的最邻近顶点和最邻近距离。3号顶点到U集合的最邻近点为7,最邻近距离为4;4号顶点到U集合的最邻近点为7,最邻近距离为9;5号顶点到U集合的最邻近点为7,最邻近距离为16;6号顶点到U集合的最邻近点为7,最邻近距离为25。

(9)找最小

在集合VU={3,4,5,6}中,依照贪心策略寻找VU集合中lowcost最小的顶点t,如图2-80所示。

..\17-0245 图\0288.tif

图2-80 lowcost[]数组

找到最小值为4,对应的结点t=3。

选中的边和结点如图2-81所示。

图2-81 最小生成树求解过程

(10)加入U战队

将顶点t加入集合U ={1,2,3,7},同时更新VU={4,5,6}。

(11)更新

刚刚找到了到U集合的最邻近点t =3,那么对t在集合VU中每一个邻接点j,都可以借助t更新。我们从图或邻接矩阵可以看出,3号结点在集合VU中的邻接点是4号结点:

C[3][4]=15>lowcost[4]=9,不更新。

closest[j]和lowcost[j]数组不改变。

更新后如图2-82所示。

..\17-0245 图\0290.tif

图2-82 最小生成树求解过程

closest[j]和lowcost[j]分别表示VU集合中顶点jU集合的最邻近顶点和最邻近距离。4号顶点到U集合的最邻近点为7,最邻近距离为9;5号顶点到U集合的最邻近点为7,最邻近距离为16;6号顶点到U集合的最邻近点为7,最邻近距离为25。

(12)找最小

在集合VU={4,5,6}中,依照贪心策略寻找VU集合中lowcost最小的顶点t,如图2-83所示。

..\17-0245 图\0291.tif

图2-83 lowcost[]数组

找到最小值为9,对应的结点t=4。

选中的边和结点如图2-84所示。

图2-84 最小生成树求解过程

(13)加入U战队

将顶点t加入集合U ={1,2,3,4,7},同时更新VU={5,6}。

(14)更新

刚刚找到了到U集合的最邻近点t =4,那么对t在集合VU中每一个邻接点j,都可以借助t更新。我们从图或邻接矩阵可以看出,4号结点在集合VU中的邻接点是5号结点:

C[4][5]=3<lowcost[5]=16,更新最邻近距离lowcost[5]=3,最邻近点closest[5]=4;

更新后的closest[j]和lowcost[j]数组如图2-85和图2-86所示。

图2-85 closest[]数组

图2-86 lowcost[]数组

更新后如图2-87所示。

..\17-0245 图\0295.tif

图2-87 最小生成树求解过程

closest[j]和lowcost[j]分别表示VU集合中顶点jU集合的最邻近顶点和最邻近距离。5号顶点到U集合的最邻近点为4,最邻近距离为3;6号顶点到U集合的最邻近点为7,最邻近距离为25。

(15)找最小

在集合VU={5,6}中,依照贪心策略寻找VU集合中lowcost最小的顶点t,如图2-88所示。

..\17-0245 图\0296.tif

图2-88 lowcost[]数组

找到最小值为3,对应的结点t=5。

选中的边和结点如图2-89所示。

图2-89 最小生成树求解过程

(16)加入U战队

将顶点t加入集合U={1,2,3,4,5,7},同时更新VU={6}。

(17)更新

刚刚找到了到U集合的最邻近点t =5,那么对t在集合VU中每一个邻接点j,都可以借助t更新。我们从图或邻接矩阵可以看出,5号结点在集合VU中的邻接点是6号结点:

C[5][6]=17<lowcost[6]=25,更新最邻近距离lowcost[6]=17,最邻近点closest[6]=5;

更新后的closest[j]和lowcost[j]数组如图2-90和图2-91所示。

图2-90 closest[]数组

图2-91 lowcost[]数组

更新后如图2-92所示。

..\17-0245 图\02100.tif

图2-92 最小生成树求解过程

closest[j]和lowcost[j]分别表示VU集合中顶点jU集合的最邻近顶点和最邻近距离。6号顶点到U集合的最邻近点为5,最邻近距离为17。

(18)找最小

在集合VU={6}中,依照贪心策略寻找VU集合中lowcost最小的顶点t,如图2-93所示。

..\17-0245 图\02101.tif

图2-93 lowcost[]数组

找到最小值为17,对应的结点t=6。

选中的边和结点如图2-94所示。

图2-94 最小生成树求解过程

(19)加入U战队

将顶点t加入集合U ={1,2,3,4,5,6,7},同时更新VU={}。

(20)更新

刚刚找到了到U集合的最邻近点t =6,那么对t在集合VU中每一个邻接点j,都可以借t更新。我们从图2-94可以看出,6号结点在集合VU中无邻接点,因为VU={}。

closest[j]和lowcost[j]数组如图2-95和图2-96所示。

..\17-0245 图\02103.tif

图2-95 closest[]数组

..\17-0245 图\02104.tif

图2-96 lowcost[]数组

得到的最小生成树如图2-97所示。

图2-97 最小生成树

最小生成树权值之和为57,即把lowcost数组中的值全部加起来。

2.7.4 伪代码详解

(1)初始化。s[1]=true,初始化数组closest,除了u0外其余顶点最邻近点均为u0,表示VU中的顶点到集合U的最临近点均为u0;初始代数组lowcostu0VU中的顶点的边值,无边相连则为∞(无穷大)。

s[u0] = true; //初始时,集合中U只有一个元素,即顶点u0
for(i = 1; i <= n; i++) 
{
     if(i != u0) //除u0之外的顶点
     {
          lowcost[i] = c[u0][i];   //u0到其它顶点的边值
          closest[i] = u0;  //最邻近点初始化为u0
          s[i] = false;  //初始化u0之外的顶点不属于U集合,即属于V-U集合
     }
      else
          lowcost[i] =0;
}

(2)在集合VU中寻找距离集合U最近的顶点t

int temp = INF;
int t = u0;
for(j = 1; j <= n; j++) //在集合中V-U中寻找距离集合U最近的顶点t
{
    if((!s[j]) && (lowcost[j] < temp)) //!s[j] 表示j结点在V-U集合中
    { 
        t = j;
        temp = lowcost[j];
    }
}
if(t == u0) //找不到t,跳出循环
   break;

(3)更新lowcostclosest数组。

s[t] = true;     //否则,将t加入集合U
for(j = 1; j <= n; j++)  //更新lowcost和closest
{
    if((!s[j]) && (c[t][j] < lowcost[j])) // !s[j] 表示j结点在V-U集合中
                                          //t到j的边值小于当前的最邻近值
    {
         lowcost[j] = c[t][j]; //更新j的最邻近值为t到j的边值
         closest[j] = t;    //更新j的最邻近点为t
    }
}

2.7.5 实战演练

//program 2-7
#include <iostream>
using namespace std;
const int INF = 0x3fffffff;
const int N = 100;
bool s[N];
int closest[N];
int lowcost[N];
void Prim(int n, int u0, int c[N][N]) 
{  //顶点个数n、开始顶点u0、带权邻接矩阵C[n][n]
  //如果s[i]=true,说明顶点i已加入最小生成树
  //的顶点集合U;否则顶点i属于集合V-U
  //将最后的相关的最小权值传递到数组lowcost
  s[u0] = true; //初始时,集合中U只有一个元素,即顶点u0
  int i;
  int j;
  for(i = 1; i <= n; i++)//①
  {
       if(i != u0) 
       {
            lowcost[i] = c[u0][i];
            closest[i] = u0;
            s[i] = false;
       }
       else
            lowcost[i] =0;
  }
  for(i = 1; i <= n; i++)  //②
  {
       int temp = INF;
       int t = u0;
       for(j = 1; j <= n; j++) //③在集合中V-u中寻找距离集合U最近的顶点t
       { 
           if((!s[j]) && (lowcost[j] < temp)) 
           {
                t = j;
                temp = lowcost[j];
           }
       }
      if(t == u0)
         break;       //找不到t,跳出循环
      s[t] = true;    //否则,讲t加入集合U
      for(j = 1; j <= n; j++)  //④更新lowcost和closest
      {       
          if((!s[j]) && (c[t][j] < lowcost[j]))
          {
              lowcost[j] = c[t][j];
              closest[j] = t;
          }
      }
    }
}
int main()
{
    int n, c[N][N], m, u, v, w;
    int u0;
    cout <<"输入结点数n和边数m:"<<endl;
    cin >> n >> m;
    int sumcost = 0;
    for(int i = 1; i <= n; i++) 
       for(int j = 1; j <= n; j++) 
          c[i][j] = INF;
    cout <<"输入结点数u,v和边值w:"<<endl;
    for(int i=1; i<=m; i++) 
    {
        cin >> u >> v >> w;
        c[u][v] = c[v][u] = w;
    }
    cout <<"输入任一结点u0:"<<endl;
    cin >> u0 ;
    //计算最后的lowcost的总和,即为最后要求的最小的费用之和
    Prim(n, u0, c);
    cout <<"数组lowcost的内容为:"<<endl;
    for(int i = 1; i <= n; i++)
        cout << lowcost[i] << " ";
    cout << endl;
    for(int i = 1; i <= n; i++)
                sumcost += lowcost[i];
    cout << "最小的花费是:" << sumcost << endl << endl;
    return 0;
}

算法实现和测试

(1)运行环境

Code::Blocks

(2)输入

输入结点数n和边数m
7 12
输入结点数uv和边值w
1 2 23
1 6 28
1 7 36
2 3 20
2 7 1
3 4 15
3 7 4
4 5 3
4 7 9
5 6 17
5 7 16
6 7 25
输入任一结点u0
1

(3)输出

数组lowcost的内容为:
0 23 4 9 3 17 1
最小的花费是:57

2.7.6 算法解析

(1)时间复杂度:在Prim(int n,int u0,int c[N][N])算法中,一共有4个for语句,第①个for语句的执行次数为n,第②个for语句里面嵌套了两个for语句③、④,它们的执行次数均为n,对算法的运行时间贡献最大。当外层循环标号为1时,③、④语句在内层循环的控制下均执行n次,外层循环②从1~n。因此,该语句的执行次数为n*n=n²,算法的时间复杂度为O(n²)。

(2)空间复杂度:算法所需要的辅助空间包含ijlowcostclosest,则算法的空间复杂度是O(n)。

该算法可以从两个方面优化:

(1)for语句③找lowcost最小值时使用优先队列,每次出队一个最小值,时间复杂度为logn,执行n次,总时间复杂度为On logn)。

(2)for语句④更新lowcostclosest数据时,如果图采用邻接表存储,每次只检查t的邻接边,不用从1~n检查,检查更新的次数为E(边数),每次更新数据入队,入队的时间复杂度为logn,这样更新的时间复杂度为OElogn)。


猜你喜欢

转载自blog.csdn.net/rainchxy/article/details/78779429