说说最小生成树(Minimum Spanning Tree)
minimum spanning tree(MST)
最小生成树是连通无向带权图的一个子图,要求
能够连接图中的所有顶点、无环、路径的权重和为所有路径中最小的.
graph-cut
对图的一个切割或者叫切断,会使图分离成为两个不相连的顶点集.
它基于树的两个基本属性:
为树的任意两个节点间添加一条边,会在树中形成一个环.
删去树中的一条边,会将原树分离成两棵不相连的树.
crossing edge
有了切断的概念,很容易就会问到,被切开的那些边是什么?
切断造成了两个不相连的顶点集,而切断的操作施加在这些边上,那么这些边本身就横跨了两个顶点集.
prim’s algorithm
最常用的一种求MST的算法是普利姆算法
它是一种贪心算法.
它搜索的方向是从某一个顶点开始,对所有邻接的边进行考察,然后将最轻的边(权重最小的边)加入MST.
决定它下一步搜索方向的,是最轻边的另一端的顶点,从这个顶点开始,它重复上一步,直到所有的顶点访问完毕.
由于普利姆算法每次都贪心地找当前看来最轻的边,它不会综合考量全局,所以它是一种贪心算法.
由于它从最轻边的另一个端点继续它的搜索之旅,所以每一步求出的在MST中的边都会自然地连成一颗树,直到最后长大为MST.
PriorityQueue
在普利姆算法中,一个重要的操作是挑选最轻边,所以一个优先队列可以对这个操作进行优化支持.
优先队列通常基于堆来实现,比如二叉堆.它可以给我们对数级的时间返回队头元素,而这个队头元素具有最高的优先级.
如果我们规定越轻的边的优先级越高,那么一个小根堆实现的升序优先队列就可以用在普利姆算法中.
lazy prim
普利姆算法在找到一条最轻边(v,w)后,这条边就不应该在以后的搜索中成为一个备选,同时点w也被加入了MST,那么优先队列中所有还未访问的但和点w连着的那些边都成了废弃边,它们不应该被再次考察.
那么如何处理这种不再备选的边?
我们可以删除这些边,但是删除这条边后,边的另一端顶点还没有访问,我们的删除操作会波及那些还没有访问的顶点.
那么,一个比较懒而直观的方式是
推迟对所有的废弃边的删除操作,任由它们在优先队列中,作为队列一员参与队列的每次调整操作.
采取懒惰方式的普利姆算法的时间复杂度是O(ElogE).从这里看出,它对边比较稠密的图来说,是低效的.
如果要了解懒惰方式的的普利姆算法,请参考:普利姆算法lazy实现
eager prim
以懒惰方式处理不再备选的边,会使得算法多次考察这样的边,影响了效率.
有没有方法对它进行改进?
考虑一下,我们在找出一条最轻边并将它加入MST后,实际上使得其它的还没有被访问的边被访问的机会增加了.
而我们只对最轻边感兴趣,所以在考察一个顶点时,只需要在优先队列中维护一条目前已知的到这个顶点的最短距离或最小权重就可以了.
为了不将废弃边加入队列,我们需要以索引方式来组织优先队列.
关于索引式的优先队列,请参考:索引式 优先队列
以积极方式队列废弃边的算法,请参考:普利姆算法eager实现
普利姆算法的eager实现的时间复杂度是最差O(ElogV )
kruskal’s algorithm
克鲁斯卡尔算法是求MST的另一种常用算法,它非常简单:
1.每一步都挑选当前看来最轻的边.如果这条边的两个端点的加入,没有在mst中造成回路,将这条边加入mst.
2.对所有的边重复上一步.
算法中比较关键的一个操作是判断是否形成回路(cycle),这可以借助并查集来做,请参考:快速并查集
克鲁斯卡尔算法的时间复杂度为O(ElogE).
这里是一个完成的算法实现,请参考:克鲁斯卡尔算法实现
综述
最小生成树和最小生成森林(minimum spanning forest)技术,对于连通性考察很有意义.
克鲁斯卡尔算法和懒惰方式的普利姆算法形式简单、易于理解,适合处理边不稠密的图.
积极方式的普利姆算法,初次接触会不易理解.但它效率高于懒惰方式,平均适用性更好.
求MST的贪心算法,都离不开优先队列或堆的支持,堆的操作效率决定了这些算法的效率.
如果用斐波那契堆来实现优先队列,则decrease-key操作的摊还代价是O(1 ),普里姆算法将可优化到:O(E+VlogV ).