以前学运筹学的时候,第一次接触到了贪婪算法,而对于它体现最好的应用貌似就是求最小生成树上了。后来大学时学习算法,看到了贪恋算法在最小生成树上的实现方式,感觉到贪恋的思想真是直观,简洁。但是从那时起到现在对于最小生成树算法的认识也觉得只此一种(后来知道它是Kruskal算法)。
直到这学期上课,看了Sanjoy Dasgupta, Christos Papadimitriou, Umesh Vazirani三人一起出版的 Algorithms (以下用作者姓的首字母指示该书,即DPV) 之后,才察觉自己的对贪婪算法的认识还处于初级阶段。之后在教授给的作业上,又发现了贪婪算法在求最小生成树上的另一种体现方式。所以在此把他们归总起来。
这个算法就是最常被提到和使用的算法。
文字表述(摘自Wikipedia):
-
新建图G,G中拥有原图中相同的节点,但没有边
-
将原图中所有的边按权值从小到大排序
-
从权值最小的边开始,如果这条边连接的两个节点于图G中不在同一个连通分量中,则添加这条边到图G中
-
重复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): 合并分别包含元素x和y的两个集合
通过辅助函数,我们就可以确定各个节点是否已经组合在了一个连通图,当所有的节点都在一个连通图中时,我们就得到了最小生成树。
在DPV一书中,给出了这些辅助函数的一种实现方式,同时还对union函数进行了优化,不过这一部分不是最小生成树算法的主体了,更多的是在研究数据结构方面对算法的贡献。
该算法的思想是从已经形成树的部分出发,一步步向外扩张,找到离已形成连通图的部分“最近”的点将其加进来。
文字表述(摘自Wikipedia):
从单一顶点开始,prime算法按照以下步骤逐步扩大树中所含顶点的数目,直到遍及连通图的所有顶点。
-
输入:一个加权连通图,其中顶点集合为V,边集合为E;
-
初始化:Vnew= {x},其中x为集合V中的任一节点(起始点),Enew= {};
-
重复下列操作,直到Vnew= V:
-
在集合E中选取权值最小的边(u,v),其中u为集合Vnew中的元素,而v则不是(如果存在有多条满足前述条件即具有相同权值的边,则可任意选取其中之一);
-
将v加入集合Vnew中,将(u,v)加入集合Enew中;
-
-
输出:使用集合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算法而言,它不止从当前一次连通图向外扩展,而是从多个连通图上同时向外扩展。所以看起来它会执行的很快。
不过它比其他两种算法有多一点限制,就是每一条边的权重必须各不相同
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'