一个无向连通图G=(V,E),对于每条边(u,v) ∈E,赋予其权重w(u,v),希望找到一个无环子集T⊆E,既能够将所有的结点连接起来,又具有最小的权重。由于T是无环的,并且连通所有的结点,因此,T必然是一棵树,这样的树为生成树,因为是由图G所生成的,称求取该生成树的问题为最小生成树问题。
求解最小生成树问题的两种通用算法是Kruskal算法和Prim算法,两种算法都采用贪心算法设计思想。
假定有一个连通无向图G=(V,E)和权重函数w:E→R,我们希望找出图G中的一棵最小生成树。
采用贪心思想可以由下面的通用方法来描述。该通用方法在每个时刻生长最小生成树的一条边,并在整个策略的实施过程,管理一个遵守下述循环不变式的边集合A:
在每遍循环之前,A是某棵最小生成树的一个子集。
在每一步,我们要做的事情是选择一条边(u,v),将其加入到集合A中,使得A不违反循环不变式,即A∪{(u,v)}也是某棵最小生成树的子集。由于我们可以安全地将这种边加入到集合A而不会破坏A的循环不变式,因此称这样的边为集合A的安全边。
GENERIC_MST(G, w)
A = NULL
while A does not form a spanning tree
find an edge(u,v) that is safe for A
A = A∪{(u,v)}
return A
在寻找安全边之前,先了解一些定义。
无向图G=(V,E)的一个切割(S,V-S)是集合V的一个划分,如果一条边(u,v) ∈E的一个端点位于集合S,另一个端点位于集合V-S,则称该条边横跨切割(S,V-S)。如果集合A中不存在横跨该切割的边,则称该切割尊重集合A。在横跨一个切割的所有边中,权重最小的边称为轻量级边。一般,如果一条边是满足某个性质的所有边中权重最小的,则称该条边是满足给定性质的一条轻量级边。一个切割的例子如下图所示:
由上,可得到安全边规则
Kruskal算法和Prim算法使用的是上述规则的推论:
Kruskal算法找到安全边的办法是,在所有连接森林中两棵不同树的边里面,找到权重最小的边(u,v)。设C1和C2为边(u,v)所连接的两棵树。由于边(u,v)一定是连接C1和其他某棵树的一条轻量级边,边(u,v)是C1的一条安全边。
操作FIND_SET(u)用来返回包含元素u的集合的代表元素,可以通过测试FIND_SET(u)是否等于FIND_SET(v)来判断结点来判断结点u和节点v是否属于同一棵树。Kruskal算法使用UNION过程来对两棵树进行合并。
MST_KRUSKAL(G, w)
A = NULL
for each vertex v ∈ G.V
MAKE_SET(v)
sort the edges of G.E into nondereasing order by weight w
for each edge(u,v) ∈ G.E, taken in nondecreasing order by weight
if FIND_SET(u) ≠ FIND_SET(v)
A = A ∪ {(u, v)}
UNION(u, v)
return A
Prim算法所具有的一个性质是集合A中的边总是构成一棵树,这棵树从一个任意的根结点r开始,一直长大到覆盖V中所有结点时为止。算法每一步在连接集合A和A之外的结点的所有边中,选择一条轻量级边加入到A中。当算法终止时,A中的边形成一棵最小生成树。
连通图G和最小生成树的根结点r将作为算法的输入。所有不再树A中的结点存放在一个基于key属性的最小优先队列Q中。对于每个结点v,属性v.key保存的是连接v和树中结点的所有边中最小边的权重。如果不存在这样的边,则v.key=∞。属性v.π给出的是结点v在树中的父结点。
MST_PRIM(G, w, r)
for each u ∈ G.V
u.key = ∞
u.π = NULL
r.key = 0
Q = G.V
while Q ≠ NULL
u = EXTRACT_MIN(Q)
for each v ∈ G.Adj[u]
if v ∈ Q and w(u,v) < v.key
v.π = u
v.key = w(u,v)