Loading

最小生成树

最小生成树是一个图问题。

我们有一个带权重的无向图,找到一个权重最低的路径连通无向图中的所有节点,这条路径如果展开看的话就是一棵树,这棵树就是最小生成树。

权重为边的一个属性,在最小生成树问题里,你可以理解为如果要通过这条边所需要的花销,当然权重具体表达的含义还得看具体问题。比如在寻找最短路径问题中,权重可以理解为两个节点之间的距离。

图示就是一个最小生成树的例子,阴影就是最小生成树的边。最小生成树不唯一,例如上图删除(b,c),加入边(a,h)也是一颗最小生成树。

嘶,看到这就莫名想到贪心算法哈,没错,即将讨论的两个常见的最小生成树的算法都是贪心逻辑。贪心算法可以看我的另一篇博文:贪心算法

通用算法

我们有这样一个最小生成树的通用算法:

GENERIC-MST(G,w)
    A = ∅
    while A不是一个生成树
        找一个边(u,v) 此边是A的安全边
        A = A ∪ {(u,v)}
    return A

算法第一行初始化A为一个空集,然后进入循环,如果A不是一个生成树,就一直循环,循环体里找一个边(u,v)是A的安全边,把它加入集合A,直到A是一个生成树,就返回。

这是一个贪心策略,每次找一个当前情况下A的安全边,这条安全边肯定存在,当A中的边已经能够连通图G中所有节点了,A肯定会构成一个最小生成树。

我们来解释一下安全边的定义然后在说明以下此贪心算法的正确性。

安全边

我们将无向图G切割,如下图

首先,看下算法导论的解释。如果此次切割没有切到集合A中的边,称切割尊重集合A。在这次切割切到的所有边中,权重最小的边就是轻量级边。它不一定是惟一的,比如上图中的(a,h)(b,c)。像这样一条轻量级边就是对A安全的,称为安全边。这里要注意一定要满足几条性质。

  1. 切割尊重集合A
  2. 切割到的边中最小的一条边是轻量级边

其实什么安全边,又切割的,就是选一个不在A中的并且能和A中已有边构成树(而不是环)的权重最小的边

什么?这个算法为啥正确?选一个权重最小的总比选大的好嘛。。。所有权重最小的边构成的生成树难道不是最好的?

你看哈,假如你想去A市的10家超市,有20条路设在这些超市之间,它们之间能组合出一条或多条连通的路你可以选择,那你当然是尽量不绕弯(不走重复路),尽量走短的,尽量一次走完了。那切割的过程就是尽量不绕弯,并且尽量一次走完,然后选边的过程就是走短的嘛。

算法的奥妙在于如何选这条安全边,毕竟计算机可不像你能划线分割,就算硬写出一个算法实现了划线,那在数据量巨大的图里工作也是不太合适的。

Kruskal算法

Kruskal算法把图中的所有顶点看成一片森林,最开始每个顶点是一颗单独的树,就是说森林里所有顶点都是单个的,不相连。

把图中所有边从小到大排序,依次选择,选择的条件是边连接的两个节点属于不同的树。每次选择到一条这样的边,这条边就是最小生成树的一条边,把这两棵树合并即可。

概算法依赖不相交集合数据结构,API如下:

// 具体见算法导论 高技数据结构篇 21.1
MAKE-SET(u) 创建一个只包含元素u的集合
FIND-SET(u) 返回包含元素u的集合代表元素
UNION(u,v)  合并两个集合
MST-KRUSKAL(G,w)
    A = ∅
    for 图G里的每一个顶点v
        MAKE-SET(v)
    
    edgeSet = 根据边的权重w按从小到大排序图里的每条边
    for edge in edgeSet
        // 判断u v是否在一棵树中
        if FIND-SET(u) != FIND-SET(v)
            A = A ∪ {(u,v)}
            UNION(u,v)

    return A

该算法时间复杂度依赖不相交集合数据结构,不展开分析。

如下是Kruskal算法在一个图上的执行过程

Prim算法

Prim从r开始,每步选择一个连接集合A和集合A之外的节点的最小边,最终推进到树连通整个图

如下是Prim算法的工作过程

看得出和Kruskal算法的差别,Kruskal算法并不限制选择的边必须连接A和集合A之外。

Prim算法借助一个基于key属性的最小优先队列里(EXTRACT-MIN方法总是弹出key属性最小的元素),对于节点v,v.key保存连接v和树中节点的所有边中最小边的权重。如果不存在这样的边,则v.key=∞。v.PI则是v在树中的父节点。

MST-PRIM(G,w,r)
    for 图G中的每个节点u
        u.key = ∞
        u.PI = NIL
    r.key = 0
    Q = G中节点集合
    while Q != ∅
        u = EXTRACT-MIN(Q)
        for v in G.Adj[u]
            if v in Q and w(u,v)<v.key
                v.PI = u
                v.key = w(u,v)

参考资料

posted @ 2020-02-17 11:34  yudoge  阅读(444)  评论(0编辑  收藏  举报