最小生成树算法

以前学运筹学的时候,第一次接触到了贪婪算法,而对于它体现最好的应用貌似就是求最小生成树上了。后来大学时学习算法,看到了贪恋算法在最小生成树上的实现方式,感觉到贪恋的思想真是直观,简洁。但是从那时起到现在对于最小生成树算法的认识也觉得只此一种(后来知道它是Kruskal算法)。

直到这学期上课,看了Sanjoy Dasgupta, Christos Papadimitriou, Umesh Vazirani三人一起出版的 Algorithms (以下用作者姓的首字母指示该书,即DPV) 之后,才察觉自己的对贪婪算法的认识还处于初级阶段。之后在教授给的作业上,又发现了贪婪算法在求最小生成树上的另一种体现方式。所以在此把他们归总起来。

 

这个算法就是最常被提到和使用的算法。

 

文字表述(摘自Wikipedia:

  1. 新建图GG中拥有原图中相同的节点,但没有边

  2. 将原图中所有的边按权值从小到大排序

  3. 从权值最小的边开始,如果这条边连接的两个节点于图G中不在同一个连通分量中,则添加这条边到图G中

  4. 重复3,直至图G中所有的节点都在同一个连通分量中

 

procedure kruskal(G,w)

Input: A connected undirected graph G = (V;E) with edge weights we

Output: A minimum spanning tree defined by the edges X

for all u ∈ V:

make set(u)

X = {}

Sort the edges E byweight

//每次选取权重最小的边作为操作对象

for all edges {u,v}∈ E, in increasing order of weight:

  //如果边的两个端点在不同的集合中,那么我们可以将该边加进来,

  //使集合两个连通组件相互连通

  if find(u) ≠find(v):

  add edge {u,v} toX

  union(u,v)

  //如果两个端点已经在同一个连通图中,那么加进该边就会形成环

  //因此舍弃该边

  if find(u) =find(v): do nothing

 

 

辅助函数:

makeset(x):构建一个只包含节点x的集合

find(x):找出节点所属的集合

union(x;y): 合并分别包含元素xy的两个集合

 

通过辅助函数,我们就可以确定各个节点是否已经组合在了一个连通图,当所有的节点都在一个连通图中时,我们就得到了最小生成树。

DPV一书中,给出了这些辅助函数的一种实现方式,同时还对union函数进行了优化,不过这一部分不是最小生成树算法的主体了,更多的是在研究数据结构方面对算法的贡献。


 


 

该算法的思想是从已经形成树的部分出发,一步步向外扩张,找到离已形成连通图的部分“最近”的点将其加进来。

 

 

文字表述(摘自Wikipedia):

从单一顶点开始,prime算法按照以下步骤逐步扩大树中所含顶点的数目,直到遍及连通图的所有顶点。

  1. 输入:一个加权连通图,其中顶点集合为V,边集合为E;

  2. 初始化:Vnew= {x},其中x为集合V中的任一节点(起始点),Enew= {};

  3. 重复下列操作,直到Vnew= V:

    1. 在集合E中选取权值最小的边(u,v),其中u为集合Vnew中的元素,而v则不是(如果存在有多条满足前述条件即具有相同权值的边,则可任意选取其中之一);

    2. 将v加入集合Vnew中,将(u,v)加入集合Enew中;

  4. 输出:使用集合Vnew和Enew来描述所得到的最小生成树。

 

procedure prim(G,w)

Input: A connected undirected graph G = (V;E) with edge weights we

Output: A minimum spanning tree defined by the array prev

for all u ∈ V :

  cost(u)= ∞

  prev(u)= nil

Pickany initial node u0

  cost(u0)= 0

H = makequeue(V) (priority queue, using cost-values as keys)

while H is not empty:

  v = deletemin(H)

  foreach (v; z) ∈ E:

    if cost(z) > w(v; z):

      cost(z)= w(v; z) //<*>

      prev(z)= v

      decreasekey(H;z)

 

对比求最短路径的Dijkstra算法,我们会发现他们有极高的相似度

 

procedure dijkstra(G;l; s)

Input: Graph G = (V;E), directed or undirected; positive edge lengths {le : e∈E};vertex s ∈ V

Output:For all vertices u reachable from s, dist(u) is set to the distance from s to u.

for all u∈V :

  dist(u)= 1

  prev(u)= nil

dist(s)= 0

H = makequeue(V) (using dist-values as keys)

while|H|>0 :

  u = deletemin(H)

  for all edges (u; v) ∈ E:

  if dist(v) > dist(u) + l(u; v):

    dist(v)= dist(u) + l(u; v) //<*>

    prev(v)= u

    decreasekey(H;v)

 

除了<*>的部分,Prim是直接将边的权重赋上,Dijkstra则是加上前序的累积权重。所以理解和算法记忆上(其实理解了也不用可以去记忆算法的具体实现了,不过对于初学的我还是需要对这些主要的算法做强化记忆),在懂了Dijkstra之后看Prim就不会有很大的难度。

 

Kruskal和Prim算法其实也还挺常见的,在网上也能搜索到很多相关的内容,但是Borůvka算法提到的就比较少了,在DPV中也没有讲到,我也是在作业中看到它的。

在我理解下,它相对于Kruskal算法来讲,每次(在while循环中)不止取出一条边进行,及它每次很贪婪很多条边加入到集合中;相对于Prim算法而言,它不止从当前一次连通图向外扩展,而是从多个连通图上同时向外扩展。所以看起来它会执行的很快。

不过它比其他两种算法有多一点限制,就是每一条边的权重必须各不相同

procedure boruvka(G,w)

Input: A connected undirected graph G = (V;E) with edge weights we

Output: A minimum spanning tree defined by the graph T = {V; E'}

E'= {}

while|E'| < |V|-1:

  let C1, ... , Ck be the connected components of T.

  //相对于kruskal算法一次取一条边,该算法每次会取到多条边

  //当然其中自然会有重复的边

  for i = 1 to k:

    ei= min-cost{(v,u) ∈E |v∈Ci&&u ∉ Ci}

  //对于重复的边,根据集合的唯一性将自动舍弃它们

  for i = 1 to k:

    add edge ei to E'

 


 


posted @ 2010-12-03 06:39  Ider  阅读(7205)  评论(2编辑  收藏  举报

我的围脖

Fly Me