1.什么是最小生成树
树是一种特殊的图。在了解构造最小生成树之前,首先要弄清楚最小生成树的概念。如图所示,分别从“树”,“生成”,“最小”这三个字眼出发,来分析概念。
上图右侧给出了四个顶点之间的完全图,其余都是该完全图的生成树,可见生成树是不唯一的。它需要满足|V|个顶点只有|V|-1条边,且无回路出现(因为生成的结构是树)。
通过如上分析,我们可以看出存在最小生成树是图连通的充分必要条件。
2.用Prim算法构建最小生成树
在了解了最小生成树的概念之后,开始介绍用Prim算法来构成最小生成树。谈到该算法就必须先说说贪心算法。贪心算法中的“贪”就是说每一步都要最好的。那么在这个问题下什么才是最好的?答案是权重最小的边。
接着,为了保证结构为生成树,需要一定的约束条件。具体如下:
Prim算法的描述:从图中的任意一点出发,从最初只有这个顶点的“初始树”开始,不断加入边和相关顶点,逐渐从一棵小树长成“最小生成树”。
具体例子:
图 12
如图12(a)所示,该算法将顶点V1作为根结点(该点是随机挑的),首先将其收录。接着“要走最好的一步”,也就是要从与该点直接相连的边中挑一条权重最小的,也就是权重为1的边。将这条边收录之后,同时将与该边相关联的另一个顶点V4也收录进来。
同理,如图12(b)所示,以V1,V4这两个顶点形成的树为基础,再向外长出一条与“当前树”相关联的最小边。共有两条权重为2的边满足要求。这种情况下权重相同则无先后收录顺序可言。先加入该树的边是与顶点V2相连的边,生成一个**“新树”。
接着,如图12(c)所示,与V1,V2,V4形成的树相关的边中最小边为刚才待收录的边**。将其收录其中,同时收录该边相连的另一个结点V3。
然后,如图12(d)所示,此时并没有收录与当前树相关的权重为3的最小边,因为如果将其收录进去,就会形成一条回路,这样就不能满足生成树的要求了。对于顶点V1,V3相连的边也是如此。那么符合要求的另一端连着顶点V7的边。
最后,如图12(e)、(f)所示,将所有7个顶点都收录进来,而且正好只用了6条边,生长成为“最小生成树”。
3.该算法伪码描述(与Dijkstra 算法类似):
该算法首先是想到该算法首先是想到最小生成树的存储结构。可以选择邻接矩阵或是邻接表。
然后树的构建。对于这个问题,这里无需定义一个根结点,再构建起一棵树。这里用到了辅助数组parent,通过其记录每个结点的父结点的编号。对于根结点S,可以用parent[s] = -1。换句话来说,只要一看到该数列中元素等于-1,那么该结点则为根结点。
对这个辅助数组进行初始化。接着进入while循环,同Dijkstra 算法一样从未收录顶点中找到dist最小者,并将其收录。但这里的dist代表的意思有所不同,它是指该顶点到当前树的最小距离。这里同样涉及到dist值的初始化问题。分两种情况,如果顶点V与根结点S直接相连的话,则将其初始化为 E(s,V)。若不直接相连,则将其初始化为正无穷。
接着,通过 dist[V] = 0表示顶点V已经被收录进生成树当中,因为当顶点V被收录之后,它已经成为生成树的一部分,显然其与生成树的最小距离为0。
然后,进入for循环语句。同Dijkstra 算法一样,要遍历顶点V的邻接点,看其是否被收录(也就是与生成树的距离是否为0)。如果未被收录,则看顶点V与其邻接点的距离是否小于邻接点到当前生成树的最小距离。如果小于则更新邻接点的dist值,并记录该邻接点的父结点。
While循环结束的情况有两种,一种是所有的顶点均被收录,得到了最小生成树。另一种情况是未被收录的顶点的dist值均为正无穷,也就是说它们与当前生成树不相连。也就是说在循环结束后,还需要扫描一下收录的顶点个数,如果不到|V|个,则图不连通(或者说生成树不存在,两者为等价关系)
4.Prim算法的时间复杂度
首先,初始化辅助数组,其循环的执行次数为O(|V|-1)(根结点的parent值为-1,其余结点默认父结点为V0)。
接着对于最小生成树的存储结构来说,用邻接表更为合适。因为Prim算法主要是将图中的顶点分为两部分,一部分是未收录的顶点,另一部分则是被收录到最小生成树中的顶点。这两部分通过辅助数组dist联系起来,因为是要不断从未收录的顶点中挑出dist值最小者,放到最小生成树的“队伍”当中。在这个过程中,只在访问顶点,而没有对边的访问,所以该算法只和顶点数量有关,与边数无关。所以说当以邻接表为存储结构时,访问顶点的次数为O|V|次。而用邻接矩阵则需要执行O|V|^2次。显然用邻接表存储更为合适。
while语句最多循环O(|V|-1)次(最后一个被收录的顶点不需要执行while循环语句)。与Dijkstra 算法一样,这循环里面还包含着两个决定最终时间复杂度的事情,一个是从未收录的顶点中找dist值最小者,另一个是更新dist[w]的值。两者执行次数均为O(|V|-1)(最坏的情况下,更新的次数等于边数。而边数|E|又等于|V|-1,所以dist[w]最多更新O(|V|-1)次)。此处需要注意访问邻接点和访问边不是一回事,只不过在访问次数上,访问邻接点次数与边数大致相等。)所以,这两者一共执行了2(O(|V|-1)2)次。总的时间复杂度2(O(|V|-1)2)+O(|V|-1)+O|V|,即(O|V|2)。