Algorithm 版 (精华区)

发信人: Lerry (想不开·撞树), 信区: Algorithm
标  题: Kruskal算法和Prim算法 
发信站: 哈工大紫丁香 (2002年03月22日14:45:17 星期五), 站内信件

Kruskal算法和Prim算法
本节所阐述的两种最小生成树算法是上节所介绍的一般算法的细化。每个算法均采用一
特定规则来确定GENERIC-MST算法第3行所描述的安全边;在Kruskal算法中,集合A是一
森林,加大集合A中的安全边总是图中连结两不同连通支的最小权边。在Prim算法中,集
合A仅形成单棵树。添加入集合A的安全边总是连结树与一非树结点的最小权边。
Kruskal算法
Kruskal算法是直接基于上一节中给出的一般最小生成树算法的基础之上的。该算法找出
森林中连结任意两棵树的所有边中具有最小权值的边(u,v)作为安全边,并把它添加到正
在生长的森林中。设C1和C2表示边(u,v)连结的两棵树。因为(u,v)必是连C1和其他某棵
树的一条轻边,所以由推论2可知(u,v)对C1是安全边。Kruskal算法同时也是一种贪心算
法,因为算法每一步添加到森林中的边的权值都尽可能小。
Kruskal算法的实现类似于计算连通支的算法。它使用了分离集合数据结构以保持数个互
相分离的元素集合。每一集合包含当前森林中某个树的结点,操作FIND-SET(u)返回包含
u的集合的一个代表元素,因此我们可以通过FIND-SET(v)来确定两结点u和v是否属于同
一棵树,通过操作UNION来完成树与树的联结。
 MST-KRUSKAL(G,w)
1. A←?
2. for 每个结点v V[G]
3.   do MAKE-SET(v)
4. 根据权w的非递减顺序对E的边进行排序
5. for 每条边(u,v)?E,按权的非递减次序
6.   do if FIND-SET(u) ≠ FIND-SET(v)
7.        then A←A∪{(u,v)}
8.             UNION(u,v)
9  return A
Kruskal算法的工作流程如图4所示。阴影覆盖的边属于正在生成的森林A。算法按权的大
小顺序考察各边。箭头指向算法每一步所考察到的边。第1-3行初始化集合A为空集并建
立|V|棵树,每裸树包含图的一个结点。在第4行中根据其权值非递减次序对E的边进行排
序。在第5-8行的for循环中首先检查对每条边(u,v)其端点u和v是否属于同一棵树。如果
是,则把(u,v)加入森林就会形成回路,所以这时放弃边(u,v)。如果不是,则两结点分
属不同的树,由第7行把边加入集合A中,第8行对两棵树中的结点进行归并。
(a)
(b)
(c)
(d)
(e)
(f)
(g)
(h)
(i)
(j)
(k)
(l)
(m)
(n)
图4 Kruskal算法在图1所示的图上的执行流程
Kruskal算法在图G=(V,E)上的运行时间取决于分离集合这一数据结构如何实现。我们采
用在分离集合中描述的按行结合和通路压缩的启发式方法来实现分离集合森林的结构,
这是由于从渐近意义上来说,这是目前所知的最快的实现方法。初始化需占用时间O(V)
,第4行中对边进行排序需要的运行时间为O(E㏒E);对分离集的森林要进行O(E)次操作
,总共需要时间为O(Ea(E,V)),其中a函数为Ackerman函数的反函数。因为a(E,V)=O(㏒
E),所以KruskaI算法的全部运行时间为O(E㏒E)。
Prim算法
正如Kruskal算法一样,Prim算法也是第上一节中讨论的一般最小生成树算法的特例。P
rim算法的执行非常类似于寻找图的最短通路的Dijkstra算法。Prim算法的特点是集合A
中的边总是只形成单棵树。如图5所示,阴影覆盖的边属于正在生成的树,树中的结点为
黑色。在算法的每一步,树中的结点确定了图的一个割,并且通过该割的轻边被加进树
中。树从任意根结点r开始形成并逐渐生长直至该树跨越了V中的所有结点。在每一步,
连接A中某结点到V-A中某结点的轻边被加入到树中,由推论2,该规则仅加大对A安全的
边,因此当算法终止时,A中的边就成为一棵最小生成树。因为每次添加到树中的边都是
使树的权尽可能小的边,因此上述策略也是贪心的。
有效实现Prim算法的关键是设法较容易地选择一条新的边添加到由A的边所形成的树中,
在下面的伪代码中,算法的输入是连通图G和将生成的最小生成树的根r。在算法执行过
程中,不在树中的所有结点都驻留于优先级基于key域的队列Q中。对每个结点v,key[v
]是连接v到树中结点的边所具有的最小权值;按常规,若不存在这样的边则key[v]=∞。
域p[v]说明树中v的“父母”。在算法执行中,GENERIC-MST的集合A隐含地满足:
A={(v,p[v])|v?V-{r}-Q}
当算法终止时,优先队列Q为空,因此G的最小生成树A满足:
A={(v,p[v])|v? V-{r}}
(a)
(b)
(c)
(d)
(e)
(f)
(g)
(h)
(i)
图5 Prim算法在图1所示的图上的执行流程
 MST-PRIM(G,w,r)
1. Q←V[G]
2. for 每个u?Q
3.   do key[u]←∞
4. key[r]←0
5. p[r]←NIL
6. while Q≠?
7.   do u←EXTRACT-MIN(Q)
8.      for 每个v?Adj[u]
9.        do if v?Q and w(u,v)<key[v]
10.            then p[v]←u
11.                 key[v]←w(u,v)
Prim算法的工作流程如图5所示。第1-4行初始化优先队列Q使其包含所有结点,置每个结
点的key域为∞(除根r以外),r的key域被置为0。第5行初始化p[r]的值为NIL,这是由
于r没有父母。在整个算法中,集合V-Q包含正在生长的树中的结点。第7行识别出与通过
割(V-Q,Q)的一条轻边相关联的结点u?Q(第一次迭代例外,根据第4行这时u=r)。从集
合Q中去掉u后把它加入到树的结点集合V-Q中。第8-11行对与u邻接且不在树中的每个结
点v的key域和p域进行更新,这样的更新保证key[v]=w(v,p[v])且(v,p[v])是连接v到树
中某结点的一条轻边。
Prim算法的性能取决于我们如何实现优先队列Q。若用二叉堆来实现Q,我们可以使用过程
BUILD-HEAP来实现第1-4行的初始化部分,其运行时间为O(V)。循环需执行|V|次,且由
于每次EXTRACT-MIN操作需要O(㏒V)的时间,所以对EXTRACT-MIN的全部调用所占用的时
间为O(V㏒V)。第8-11行的for循环总共要执行O(E)次,这是因为所有邻接表的长度和为
2|E|。在for循环内部,第9行对队列Q的成员条件进行测试可以在常数时间内完成,这是
由于我们可以为每个结点空出1位(bit)的空间来记录该结点是否在队列Q中,并在该结点
被移出队列时随时对该位进行更新。第11行的赋值语句隐含一个对堆进行的DECREASE-K
EY操作,该操作在堆上可用0(㏒V)的时间完成。因此,Prim算法的整个运行时间为O(V㏒
V+E㏒V)=O(E㏒V),从渐近意义上来说,它和实现Kruskal算法的运行时间相同。
通过使用Fibonacci堆,Prim算法的渐近意义上的运行时间可得到改进。在Fibonacci堆
中我们己经说明,如果|V|个元素被组织成Fibonacci堆,可以在O(㏒V)的平摊时间内完
成EXTRACT-MIN操作,在O(1)的平摊时间里完成DECREASE-KEY操作(为实现第11行的代码
),因此,若我们用Fibonacci堆来实现优先队列Q,Prim算法的运行时间可以改进为O(
E+V㏒V)。


--
  不在乎天长地久,就怕你从来没有!

※ 来源:·哈工大紫丁香 bbs.hit.edu.cn·[FROM: 天外飞仙]
[百宝箱] [返回首页] [上级目录] [根目录] [返回顶部] [刷新] [返回]
Powered by KBS BBS 2.0 (http://dev.kcn.cn)
页面执行时间:3.986毫秒