最小生成树的形成
假定有一无向图 G = ( V , E ) G=(V,E) G=(V,E)和权重函数 w w w, 我们希望找出图 G G G的一颗最小生成树。
我们使用贪心算法来解决这个问题。贪心算法管理一个遵守下述循环不变式的边集和 A A A: 每轮循环之前, A A A是最小生成树的一个子集。在当轮循环,我们要做的就是选择一条边 ( u , v ) (u,v) (u,v),将其加入子集 A A A中,使 A A A仍然为最小生成树的一个子集,我们称这样的边为安全边。
找出最小生成树的关键在于如何选择安全边。在介绍具体的方法之前,我们先引入一些定义。无向图 G = ( V , E ) G=(V,E) G=(V,E)的一个切割 ( S , V − S ) (S,V-S) (S,V−S)是对集合 V V V的一个划分。如果一条边 ( u , v ) (u,v) (u,v)的一个端点位于集合 S S S,令一个端点位于集合 V − S V-S V−S,则称这条边横跨切割 ( S , V − S ) (S,V-S) (S,V−S)。如果集合 A A A中不存在横跨切割 ( S , V − S ) (S,V-S) (S,V−S)的边,则称该切割尊重集合 A A A。在横跨切割的所有边中,称权重最小的边为轻量级边。
下面我们将证明:假如切割 ( S , V ) (S,V) (S,V)尊重集合 A A A,则横跨切割的轻量级边 ( u , v ) (u,v) (u,v)对集合 A A A是一条安全边。设 T T T是一颗包含 A A A的最小生成树,且T不包含边 ( u , v ) (u,v) (u,v)。边 ( u , v ) (u,v) (u,v)与生成树 T T T中 u , v u,v u,v之间的简单路 p p p构成了一条回路。由于 ( u , v ) (u,v) (u,v)横跨切割,则 p p p中必存在一条边 ( x , y ) (x,y) (x,y)横跨切割。我们设置一个新的生成树 T ′ = T ∪ ( u , v ) − ( x , y ) T'=T\cup{(u,v)-(x,y)} T′=T∪(u,v)−(x,y)。由于 ( u , v ) (u,v) (u,v)为轻量级边,所以有 w ( u , v ) ≤ w ( x , y ) w(u,v)\le w(x,y) w(u,v)≤w(x,y),于是有 w ( T ′ ) = w ( T ) − w ( x , y ) + w ( u , v ) ≤ w ( T ) w(T')=w(T)-w(x,y)+w(u,v)\le w(T) w(T′)=w(T)−w(x,y)+w(u,v)≤w(T)。由于 T T T是最小生成树,所以 T ′ T' T′也是最小生成树。由于 A ⊆ T A\subseteq T A⊆T且 ( x , y ) ∉ A (x,y)\notin A (x,y)∈/A,所以 A + ( u , v ) ⊆ T ′ A+(u,v)\subseteq T' A+(u,v)⊆T′。由此证明, ( u , v ) (u,v) (u,v)对集合 A A A是一条安全边。
随着最小生成树算法的推进, A A A总保持着无环的状态。在算法执行的任意时刻,图 G A = ( V , A ) G_A=(V,A) GA=(V,A)是一个森林,图 G A G_A GA中的每一个连通分量是一颗树。所有对 A A A的安全边 ( u , v ) (u,v) (u,v)所连接的是 G A G_A GA中不同的连通分量。
Kruskal算法
Kruskal找到安全边的方法是,在所有连接森林中不同的两棵树的边中,找到权重最小的一个边 ( u , v ) (u,v) (u,v),时间复杂度为 O ( E log V ) O(E\log V) O(ElogV)。伪代码如下:
MST-KRUSKAL(G,w)
A = 空集
for each vertex v in G.V
MAKE-SET(v)
sort the edges of G.E into nondecreasing order by weight w
for each edge(u,v) in G.E, taken in nondecreasing order by weight
if FIND-SET(u) != FIND-SET(v)
A = A∪{(u,v)}
UNION(u,v)
return A
FIND-SET(u)用来返回集合 u u u的代表元素,UNION用来合并两个集合。
Prim算法
Prim算法中的集合 A A A总是构成一棵树,这棵树从一个任意的根节点 r r r开始,一直长大到覆盖 V V V中的所有结点为止。算法每一步在连接集合 A A A和 A A A之外结点的边中,选择一条轻量级边加入 A A A中。伪代码如下:
MST-PRIM(G,w,r)
for each u in G.V
u.key = ∞
u.pi = NIL
r.key = 0
Q = G.V
while Q != 空集
u = EXTRACT-MIN(Q)
for each v in G.Adj[u]
if v in Q and w(u,v) < v.key
v. pi = u
v.key = w(u,v)
在算法的执行过程中,所有不在 A A A中的结点都存放在一个基于 k e y key key属性的最小优先队列中 Q Q Q中。对每个结点 v v v,属性 v . k e y v.key v.key保存的是连接树 A A A中结点的所有边中的最小边的权重。
如果使用二叉堆,时间复杂度为 O ( E log V ) O(E\log V) O(ElogV);如果使用斐波那契堆,时间复杂度为 O ( E + V log V ) O(E+V\log V) O(E+VlogV)。