<>在例1 - 2及1 - 3中已考察过这个问题。因为具有n 个顶点的无向网络G的每个生成树刚好具有n-1条边,所以问题是用某种方法选择n-1条边使它们形成G的最小生成树。至少可以采用三种不同的贪婪策略来选择这n-1条边。这三种求解最小生成树的贪婪算法策略是: K r u s k a l算法,P r i m算法和S o l l i n算法。</P> # ?9 `, D; k! s8 _1 W<>1. Kruskal算法</P> 4 q6 |8 o3 b e t- |4 c<>(1) 算法思想</P> 9 T! ~* D2 ` x3 ~8 h& R% i<>K r u s k a l算法每次选择n- 1条边,所使用的贪婪准则是:从剩下的边中选择一条不会产生环路的具有最小耗费的边加入已选择的边的集合中。注意到所选取的边若产生环路则不可能形成一棵生成树。K r u s k a l算法分e 步,其中e 是网络中边的数目。按耗费递增的顺序来考虑这e 条边,每次考虑一条边。当考虑某条边时,若将其加入到已选边的集合中会出现环路,则将其抛弃,否则,将它选入。</P>" D L' J R1 v
<>考察图1-12a 中的网络。初始时没有任何边被选择。图13-12b 显示了各节点的当前状态。边( 1 , 6)是最先选入的边,它被加入到欲构建的生成树中,得到图1 3 - 1 2 c。下一步选择边( 3,4)并将其加入树中(如图1 3 - 1 2 d所示)。然后考虑边( 2,7 ),将它加入树中并不会产生环路,于是便得到图1 3 - 1 2 e。下一步考虑边( 2,3)并将其加入树中(如图1 3 - 1 2 f所示)。在其余还未考虑的边中,(7,4)具有最小耗费,因此先考虑它,将它加入正在创建的树中会产生环路,所以将其丢弃。此后将边( 5,4)加入树中,得到的树如图13-12g 所示。下一步考虑边( 7,5),由于会产生环路,将其丢弃。最后考虑边( 6,5)并将其加入树中,产生了一棵生成树,其耗费为9 9。图1 - 1 3给出了K r u s k a l算法的伪代码。</P>3 O$ D9 {% f, D+ e
1 X0 Q5 i1 g, W" y/ D; n4 [' n. x$ o- |( z4 Z
<>/ /在一个具有n 个顶点的网络中找到一棵最小生成树</P> 4 N: e L7 X' `% C" ^<>令T为所选边的集合,初始化T=</P> # _- e+ N0 U/ X<>令E 为网络中边的集合</P> ( E+ A6 c& T2 d3 }# F3 D1 @; J<>w h i l e(E≠ )&&(| T |≠n- 1 ) {</P>$ Q9 D( G0 t2 b, s* t0 \( z/ B& O
<>令(u,v)为E中代价最小的边 E=E- { (u,v) } / /从E中删除边</P>$ X- Z' r; \* U8 K
<>i f( (u,v)加入T中不会产生环路)将( u,v)加入T</P> 8 L# r$ @* ^ F/ f/ L4 d<>}</P>! F5 S K9 e D
<>i f(| T | = =n-1) T是最小耗费生成树</P>4 s7 n4 _2 i' F. k
<>e l s e 网络不是互连的,不能找到生成树</P> 7 K6 T& s1 P$ k9 L<>图13-13 Kruskao算法的伪代码</P> ' x2 Z; V3 _) u i) m5 z- \ F<p> p! s' s. R- H; h8 h8 c* g3 u
<>(2) 正确性证明</P> & D6 ?. d( D, b<>利用前述装载问题所用的转化技术可以证明图1 3 - 1 3的贪婪算法总能建立一棵最小耗费生成树。需要证明以下两点: 1) 只要存在生成树,K r u s k a l算法总能产生一棵生成树; 2) 产生的生成树具有最小耗费。令G为任意加权无向图(即G是一个无向网络)。从1 2 . 11 . 3节可知当且仅当一个无向图连通时它有生成树。而且在Kruskal 算法中被拒绝(丢弃)的边是那些会产生环路的边。删除连通图环路中的一条边所形成的图仍是连通图,因此如果G在开始时是连通的,则T与E中的边总能形成一个连通图。也就是若G开始时是连通的,算法不会终止于E= 和| T |< n- 1。</P> : k: @# X! \) S0 m. \<>现在来证明所建立的生成树T具有最小耗费。由于G具有有限棵生成树,所以它至少具有一棵最小生成树。令U为这样的一棵最小生成树, T与U都刚好有n- 1条边。如果T=U, 则T就具有最小耗费,那么不必再证明下去。因此假设T≠U,令k(k >0) 为在T中而不在U中的边的个数,当然k 也是在U中而不在T中的边的数目。 通过把U变换为T来证明U与T具有相同的耗费,这种转化可在k 步内完成。每一步使在T而不在U中的边的数目刚好减1。而且U的耗费不会因为转化而改变。经过k 步的转化得到的U将与原来的U具有相同的耗费,且转化后U中的边就是T中的边。由此可知, T具有最小耗费。每步转化包括从T中移一条边e 到U中,并从U中移出一条边f。边e 与f 的选取按如下方式进行:</P>% U6 h/ ?# o+ l. ^9 O" z2 F
<>1) 令e 是在T中而不在U中的具有最小耗费的边。由于k >0,这条边肯定存在。</P> : d1 M5 n! q; x; F1 a<>2) 当把e 加入U时,则会形成唯一的一条环路。令f 为这条环路上不在T中的任意一条边。</P> 8 I" B+ S2 x, g1 {, e! }% a<>由于T中不含环路,因此所形成的环路中至少有一条边不在T中。</P>* J; S5 R0 z& J1 b" X
<>从e 与f 的选择方法中可以看出, V=U+ {e} -{ f } 是一棵生成树,且T中恰有k- 1条边不在V中出现。现在来证明V的耗费与U的相同。显然,V的耗费等于U的耗费加上边e 的耗费再减去边f 的耗费。若e 的耗费比f 的小,则生成树V的耗费比U的耗费小,这是不可能的。如果e 的耗费高于f,在K r u s k a l算法中f 会在e 之前被考虑。由于f 不在T中,Kruskal 算法在考虑f 能否加入T时已将f 丢弃,因此f 和T中耗费小于或等于f 的边共同形成环路。通过选择e,所有这些边均在U中,因此U肯定含有环路,但是实际上这不可能,因为U是一棵生成树。e 的代价高于f 的假设将会导致矛盾。剩下的唯一的可能是e 与f 具有相同的耗费,由此可知V与U的耗费相同。</P> p) P4 U+ y# h, M/ a$ V
<>(3) 数据结构的选择及复杂性分析</P>/ e) t, i, W! M- s; Y8 h
<>为了按耗费非递减的顺序选择边,可以建立最小堆并根据需要从堆中一条一条地取出各边。当图中有e 条边时,需花(e) 的时间初始化堆及O ( l o ge) 的时间来选取每一条边。边的集合T与G中的顶点一起定义了一个由至多n 个连通子图构成的图。用顶点集合来描述每个子图,这些顶点集合没有公共顶点。为了确定边( u,v)是否会产生环路,仅需检查u,v 是否在同一个顶点集中(即处于同一子图)。如果是,则会形成一个环路;如果不是,则不会产生环路。因此对于顶点集使用两个F i n d操作就足够了。当一条边包含在T中时,2个子图将被合并成一个子图,即对两个集合执行U n i o n操作。集合的F i n d和U n i o n操作可利用8 . 1 0 . 2节的树(以及加权规则和路径压缩)来高效地执行。F i n d操作的次数最多为2e,Un i o n操作的次数最多为n- 1(若网络是连通的,则刚好是n- 1次)。加上树的初始化时间,算法中这部分的复杂性只比O (n+e) 稍大一点。</P>8 i5 _" Q3 j& |' c( J7 m/ R2 R3 |
<>对集合T所执行的唯一操作是向T中添加一条新边。T可用数组t 来实现。添加操作在数组</P> 2 \3 n4 i- U, f' Y<>的一端进行,因为最多可在T中加入n- 1条边,因此对T的操作总时间为O (n)。</P>+ r+ `" W: L* M+ Z' K% a4 s A
<>总结上述各个部分的执行时间,可得图1 3 - 1 3算法的渐进复杂性为O (n+el o ge)。</P>0 j7 z8 I! T1 ^8 ]- }
<>(4) 实现</P> ' y* O' t) l1 S: O6 i, \<>利用上述数据结构,图1 - 1 3可用C + +代码来实现。首先定义E d g e N o d e类(见程序1 3 - 6 ),它是最小堆的元素及生成树数组t 的数据类型。</P>/ I+ E# I' d7 R" o
<>程序13-6 Kruskal算法所需要的数据类型</P>6 E3 D+ l5 o( P- x* P
<P>template <CLASS T></P> 7 P- A: Y4 e1 T, W- e, n<P>class EdgeNode {</P>3 u- I+ s+ e; x- m* [, n
<P>p u b l i c :</P> 8 N5 @2 e; z; J7 m8 \- n% P! a<P>operator T() const {return weight;}</P> ( I" r+ t% c9 |<P>p r i v a t e :</P> # N/ P/ F6 g6 ]* ~& k<P>T weight;//边的高度</P>: b, z: P- {' e. N
<P>int u, v;//边的端点</P> t1 ^; b% P& f, U, u% L4 o7 }
<P>} ;</P>8 C' U, m7 @& T9 ?, L/ t
<P>为了更简单地使用8 . 1 0 . 2节的查找和合并策略,定义了U n i o n F i n d类,它的构造函数是程序8 - 1 6的初始化函数,U n i o n是程序8 - 1 6的加权合并函数,F i n d是程序8 - 1 7的路径压缩搜索函数。</P> 0 Z9 H* i, p& t) V3 u. w9 ^: g<P>为了编写与网络描述无关的代码,还定义了一个新的类U N e t Wo r k,它包含了应用于无向网络的所有函数。这个类与U n d i r e c t e d类的差别在于U n d i r e c t e d类中的函数不要求加权边,而U N e t Wo r k要求边必须带有权值。U N e t Wo r k中的成员需要利用N e t w o r k类中定义的诸如B e g i n和N e x t Ve r t e x的遍历函数。不过,新的遍历函数不仅需要返回下一个邻接的顶点,而且要返回到达这个顶点的边的权值。这些遍历函数以及有向和无向加权网络的其他函数一起构成了W N e t w o r k类(见程序1 3 - 7)。</P> & m I" z$ W, d; u- C<P>程序13-7 WNetwork类</P>& l( d( l/ q" o
<P>template<CLASS T></P>7 K7 S h5 T* \( v% K
<P>class WNetwork : virtual public Network</P>8 x. G- ~5 d5 B8 r0 H0 h" ?
<P>{</P> ' X0 u" R3 J' _<P>public :</P>0 H1 c: o1 i+ q2 [+ p
<P>virtual void First(int i, int& j, T& c)=0;</P> j8 j, I5 e% _: P% U0 ?% b
<P>virtual void Next(int i, int& j, T& c)=0;</P>' f5 }; f7 R1 j% L
<P>} ;</P> ' c. a$ V8 w, Z<P>象B e g i n和N e x t Ve r t e x一样,可在A d j a c e n c y W D i g r a p h及L i n k e d W D i g r a p h类中加入函数F i r s t与N e x t。现在A d j a c e n c y W D i g r a p h及L i n k e d W D i g r a p h类都需要从W N e t Wo r k中派生而来。由于A d j a c e n c y W G r a p h类和L i n k e d W G r a p h类需要访问U N e t w o r k的成员,所以这两个类还必须从U N e t Wo r k中派生而来。U N e t Wo r k : : K r u s k a l的代码见程序1 3 - 8,它要求将Edges() 定义为N e t Work 类的虚成员,并且把U N e t Wo r k定义为E d g e N o d e的友元)。如果没有生成树,函数返回f a l s e,否则返回t r u e。注意当返回true 时,在数组t 中返回最小耗费生成树。</P>" L5 V/ Y" f9 m0 L( G4 Q, J
<P>程序13-8 Kr u s k a l算法的C + +代码</P># a2 N- X* l; Q. @2 q
<P>template<CLASS T></P> 5 J9 f$ V' U7 [: w" `0 {<P>bool UNetwork<T>::Kruskal(EdgeNode<T> t[])</P> 3 x7 N5 s3 o) v- d- |<P>{// 使用K r u s k a l算法寻找最小耗费生成树</P> ; _4 ]' f- y' w<P>// 如果不连通则返回false</P>4 K8 j X: K1 n( ?+ W* X! r2 u
<P>// 如果连通,则在t [ 0 : n - 2 ]中返回最小生成树</P> 0 o* |7 J* Q; e4 m }<P>int n = Ve r t i c e s ( ) ;</P> 8 c: o; v2 r, [( v# F$ F5 a1 h) n<P>int e = Edges();</P>& ~! V+ h' ] f& Q$ _
<P>/ /设置网络边的数组</P> 0 H0 [7 R7 w' Q* ]6 _) x3 l<P>InitializePos(); // 图遍历器</P>3 F2 a2 ]# ~, l- \! F
<P>EdgeNode<T> *E = new EdgeNode<T> [e+1];</P> ( ^, o+ X7 V; r7 R k6 I1 V<P>int k = 0; // E的游标</P>% N. v. O+ F, p8 _) w2 q! j
<P>for (int i = 1; i <= n; i++) { // 使所有边附属于i</P>5 O0 _. B; G6 @& F5 H5 ~
<P>int j;</P>! K9 |( T( _$ S. {0 V' F
<P>T c;</P> $ i; ?! @& ]% G( Y6 s<P>First(i, j, c);</P>8 N3 M! F$ D' J0 h9 l
<P>while (j) { // j 邻接自i</P>- v4 ?; R$ ^$ ^! c6 Q
<P>if (i < j) {// 添加到达E的边</P> 4 a# D5 \) w2 X$ S<P>E[++k].weight = c;</P>+ M1 a* O# R3 W: ]- ?! h) Y, S
<P>E[k].u = i;</P>. u1 S- t3 i9 Z$ S6 w2 V0 h' K
<P>E[k].v = j;}</P> , w$ l7 ^4 C6 l/ s' L5 L% ~<P>Next(i, j, c);</P> . w$ K* W. x* U" [: _<P>}</P>/ I* ~+ r" J. i' C, N7 p
<P>}</P>- x( Q) K" o1 w- }& H
<P>// 把边放入最小堆</P> $ O ]5 A2 }' U' X<P>MinHeap<EDGENODE<T> > H(1);</P>0 x( H; `; |/ D- A7 N& v
<P>H.Initialize(E, e, e);</P>4 S0 f% ~* r j) w- _
<P>UnionFind U(n); // 合并/搜索结构</P> ( K) _# [2 |* E" @3 m0 ~+ n<P>// 根据耗费的次序来抽取边</P>) c( x/ y1 X+ D8 _. w
<P>k = 0; // 此时作为t的游标</P>% A1 H% w; e0 G- z0 ?0 T
<P>while (e && k < n - 1) {</P>8 a1 S9 _9 B7 W0 i7 V
<P>// 生成树未完成,尚有剩余边</P>0 S; j6 H" w) `* n
<P>EdgeNode<T> x;</P>9 B7 `* A; n R: n& x8 U
<P>H.DeleteMin(x); // 最小耗费边</P>, G; y2 O8 ?: ?0 m8 @/ N( H' J
<P>e - - ;</P>* c0 J3 E8 p# \. t( P( ?
<P>int a = U.Find(x.u);</P> & d0 B0 c: h- d1 Y<P>int b = U.Find(x.v);</P>3 V' b' N8 c- L/ B
<P>if (a != b) {// 选择边</P> 5 Y" g8 n7 u0 d2 y$ v6 E<P>t[k++] = x;</P># X5 n, H0 V3 [
<P>U . U n i o n ( a , b ) ; }</P> j6 k# T# w* ^- \<P>}</P>/ h3 g1 w) O$ ~1 ^3 @* A
<P>D e a c t i v a t e P o s ( ) ;</P> + I9 U: n( h! }& a<P>H . D e a c t i v a t e ( ) ;</P>8 f4 W9 Z4 q) i9 o, z/ \1 F
<P>return (k == n - 1);</P>, \. T/ s! z* b A& O9 K6 \
<P>}</P> 9 @" b7 z! @3 O. t6 H: s<P>2. Prim算法</P> & y3 j* R. w) E$ J<P>与Kr u s k a l算法类似,P r i m算法通过每次选择多条边来创建最小生成树。选择下一条边的贪婪准则是:从剩余的边中,选择一条耗费最小的边,并且它的加入应使所有入选的边仍是一棵树。最终,在所有步骤中选择的边形成一棵树。相反,在Kruskal 算法中所有入选的边集合最终形成一个森林。</P>( `- b" f9 h$ M& G% u
<P>P r i m算法从具有一个单一顶点的树T开始,这个顶点可以是原图中任意一个顶点。然后往T中加入一条代价最小的边( u , v)使Tè{ (u , v) }仍是一棵树,这种加边的步骤反复循环直到T中包含n- 1条边。注意对于边( u , v),u、v 中正好有一个顶点位于T中。P r i m算法的伪代码如图1 -1 4所示。在伪代码中也包含了所输入的图不是连通图的可能,在这种情况下没有生成树。图1 - 1 5显示了对图1-12a 使用P r i m算法的过程。把图1 - 1 4的伪代码细化为C + +程序及其正确性的证明留作练习(练习3 1)。</P>/ f7 j+ d+ {: }5 Y
<p> 9 Y6 u7 X B" u9 d: \0 ^( Y- y8 \<P>/ /假设网络中至少具有一个顶点</P> j1 m8 N# U( t* {5 t, ]. b, S6 {<P>设T为所选择的边的集合,初始化T=</P>" y* _" l2 G& [+ [
<P>设T V为已在树中的顶点的集合,置T V= { 1 }</P> 4 F% a: ?; h% B5 N* u. D& |6 |<P>令E为网络中边的集合</P>' s; o& Y0 B* d6 A( D# x+ d
<P>w h i l e(E< > ) & & (| T | < > n-1) {</P>) t# i/ q6 w6 v0 a$ E
<P>令(u , v)为最小代价边,其中u T V, v T V</P>) `% q$ L5 R' \
<P>i f(没有这种边) b re a k</P> 0 N# b0 {$ \, g; Q<P>E=E- { (u,v) } / /从E中删除此边</P> 8 h7 O4 o" O- |" Y8 W) b6 B* w<P>在T中加入边( u , v)</P>5 ]2 @4 }4 p) M8 w# F1 j- p
<P>}</P>3 ]2 c) G; u! n9 b4 Q5 B Z
<P>if (| T | = =n- 1 ) T是一棵最小生成树</P> 8 n% b: S* U% m# g) z, E<P>else 网络是不连通的,没有最小生成树</P> + g% s: x8 ~! q& h4 w. ^2 J; n<P>图13-14 Prim最小生成树算法</P>' u) D! g9 V! f4 \
<p> . T( V% @' o! g" G7 u5 N<P>如果根据每个不在T V中的顶点v 选择一个顶点n e ar (v),使得n e ar (v) ? TV 且c o st (v, n e ar (v) )的值是所有这样的n e ar (v) 节点中最小的,则实现P r i m算法的时间复杂性为O (n2 )。下一条添加到T中的边是这样的边:其cost (v, near (v)) 最小,且v T V。</P> `/ m% S& ~: ]" A
<P>3. Sollin算法</P>4 I7 y2 v1 N5 P0 Q6 t3 V& G* k
<P>S o l l i n算法每步选择若干条边。在每步开始时,所选择的边及图中的n个顶点形成一个生成树的森林。在每一步中为森林中的每棵树选择一条边,这条边刚好有一个顶点在树中且边的代价最小。将所选择的边加入要创建的生成树中。注意一个森林中的两棵树可选择同一条边,因此必须多次复制同一条边。当有多条边具有相同的耗费时,两棵树可选择与它们相连的不同的边,在这种情况下,必须丢弃其中的一条边。开始时,所选择的边的集合为空。若某一步结束时仅剩下一棵树或没有剩余的边可供选择时算法终止。</P> + }( {+ F9 y3 x' g" {' }<P>图1 - 6给出了初始状态为图1-12a 时,使用S o l l i n算法的步骤。初始入选边数为0时的情形如图13-12a 时,森林中的每棵树均是单个顶点。顶点1,2,.,7所选择的边分别是(1.6), (2,7),(3,4), (4,3), (5,4), (6,1), (7,2),其中不同的边是( 1 , 6 ),( 2 , 7 ),(3,4) 和( 5 , 4 ),将这些边加入入选边的集合后所得到的结果如图1 3 - 1 6 a所示。下一步具有顶点集{ 1 , 6 }的树选择边( 6 , 5 ),剩下的两棵树选择边( 2 , 3 ),加入这两条边后已形成一棵生成树,构建好的生成树见图1 3 - 6 b。S o l l i n算法的C + +程序实现及其正确性证明留作练习(练习3 2 )。</P>