(10)图算法2: 最小生成树, 最短路径

最小生成树(MST)问题

  • 对象: 该问题总是针对连通无向图G = (V, E);
  • 总体算法
    • 这个算法理出大概的思路, 真正实现还分为点和边两种方式;
Generic-MST(G, w)  //w是权重函数
A = ∅
while A does not form a spanning tree 
    find an edge(u,v) that is safe for A
    A = A∪(u,v)
return A  //the minimum spanning tree
  • 总体性质: A是图G=(V, E)中E的在SpanningTree中的边的集合, 如果(u,v)是横跨切割(S, V-S)的轻量级边, 其中S是SpanningTree的意思, 那么边(u, v)对于集合A是安全的;

证明: (分类讨论)
–如果(u, v)在STree中, 那么命题得证(安全边才能被收入A中);
–如果(u,v)不在STree中, 因为u,v两个点都在STree中, 那么必然在A中已经通过某种路径相连, 这条路径加上(u,v)边会形成一个环路, 且此时至少有两条边跨越了切割, 一个是(u, v), 另外一个不妨写作(x, y). 若断掉(x,y)留下(u, v) 则此时T’.w = T.w + w(u, v) – w(x, y), 因为(u, v)是轻量级边, 所以T’.w <= T.w, 所以说(u,v)边是安全的;

prim算法

  • 思路: 并点, 不断找新生成树的切割面的最短边, 是典型的贪心算法;
MST-Prim(G, w, r)  //Graph, weight, root
for each u ∈ G.V
    u.key = ∞
    u.parent = null
r.key = 0
Q = G.V  //initiate a priority queue, 初始化结束
while Q != ∅:
    u = Extract-Min(Q)  //依据的是u.key是Q中最小的;
    for each v∈G.Adj[u]:  
        if v∈Q and w(u, v)<v.key:
            v.parent = u
            v.key = w(u, v)

Note: 获取MST的时候, 只要让每个非root的结点u输出(u, u.parent)边;

  • 时间复杂度分析:
    (1) 使用二维数组W存储weight, 一维数组V存储结点: 初始化操作Θ(V);Extra-Min操作V次, 每次耗时Θ(V), 所以这个环节是Θ(V^2); 对边的操作, 总共有E条边, 每次耗时都是Θ(1), 因此这个环节Θ(E); 总体耗时因此是Θ(V^2+E);
    (2) 使用二叉堆构建最小优先队列, Extract-Min耗时总共O(VlgV), 对边的操作需要做E次Decrease-Key, 耗时总共O(ElgV); 总体耗时因此为O((V+E)lgV), 因为E>=V-1总是成立, 因此可以写作O(ElgV);
    (3) 使用斐波那契堆实现最小优先队列Q的话, Extract-Min时间仍是lgV, 但是Decrease-Key下降到O(1), 因此总体上时间是O(VlgV+E), 小于O(ElgV).

Kruskal算法

  • 思路: 并边, 不断找整个图中最短边, 只要不构成环, 即相互不连通, 就并入, 也是贪心算法;.
MST-Kruskal(G, w)
A = ∅
for each u ∈ G.V
    Make-Set(u)
sort the edges of G.E as ascending order by weight w  //到此初始化完成
for edge(u, v) of the sorted edge sequence
    if Find-Set(u) != Find-Set(v):
        A = A ∪ { (u,v) }  //add edge to A;
        Union(u, v)  //combine two trees
return A  //A is the tree;
  • 时间复杂度分析: 排序消耗了O(ElgE), 在改进过的并查集中, Find-Set总共做了E次, 消耗O(E), 而Union总共做了V次, 消耗O(Vα(V)), 其中α(V)<=4, 可以看作O(lgV), 因此Union消耗了O(VlgV), 因此并边循环总共消耗O(E+VlgV), 显然dominator是排序操作, 因此总体来说 T = O(ElgE).

网路最短路径问题

  • 对象: 带权重的有向图G = (V, E); 注意和最小生成树的主要区别在于这是有向图!!!

共用方法

  • 初始化
Initialize-Single-Source(G, s)
for each vertex v ∈ G.V:
    v.d = ∞
    v.parent = null
s.d = 0
  • 松弛操作
Relax(u, v, w)
if v.d > u.d+w(u,v):
    v.d = u.d+w(u,v)
    v.parent = u

松弛定理: 对v点, 如果从s点到vi确实存在一条最短路径, 那么只要(s, v1), (v1, v2), … (vi-1, vi)都被依次执行了relax操作, 那么vi.d = δ(s, vi). 该性质的成立与其他穿插在该序列中间的松弛操作无关.

  • 也就是说, 接下去的三个算法, 我们都要检查松弛定理是否能运用, 只要能运用, 算法是正确的;

1. 一般情况下的单源最短路径问题

  • 条件宽松: 允许出现负权重的环路;
Bellman-Ford(G, w, s) {
Initialize-Single-Source(G, s)
for i = 1~|V|-1:
    for each edge(u, v) ∈ G.E:  
        relax(u, v)
for each edge(u, v) ∈ G.E:
    if v.d >u.d+w(u, v):  //只要任何一条边仍能继续松弛, 那么说明有负权重的环路存在;
        return FALSE
return TRUE
}

正确性:
(1) 对某个点vi来说, 从s如果有一条最短路径, 那么在算法的第一个for循环, 到第i次循环, 该最短路径上的边(s, v1), (v1, v2)…(vi-1,vi)都将被松弛, 因此该算法能满足松弛定理, 使得vi.d = δ(s, vi);
(2) 最短路径上界定理: 对所有的最短路径来说, 最多只能有V-1条边, 否则将形成一个环. 因此Bellman-Ford算法只需要V-1轮全体relax就能cover到即使是最长的那条最短路径.

  • 时间复杂度分析: 主要时间消耗在V-1次轮的全体relax, 一共消耗了O(VE);

2. 有向无环图中的单源最短路径问题

  • 条件收紧: 允许使用负权重的边;
  • 借助拓扑序
DAG-Shortest-Path(G, w, s) {
topologically sort the vertices of G
Initialize-Single-Source(G, s)
for each vertex u taken by the sorted order:
    for each v of G.adj[u]:
        Relax(u, v, w)
}

正确性: 因为是按照拓扑序对所有的边进行松弛, 因此最短路径上的边(s, v1), (v1, v2)…(vi-1,vi)的边, 都会被依次松弛, 尽管中间可能穿插其他的松弛, 最后必然能实现vi.d = δ(s, vi)

迪杰斯特拉算法

  • 条件进一步收紧: 只允许非负权重的边;
  • 思路:
    对每个节点赋值∞, 每次都并一个距离最小的结点, 同时更新该结点的adj结点, 不断迭代;
Dijkstra(G, w, s) {
Initialize-Single-Source(G, s)
S = ∅
Q = G.V  //初始化完成
while Q != ∅:
    u = Extract-Min(Q)
    S = S∪{u}  //add u to S;
    for each vertex v of G.adj[u]:
        Relax(u, v, w)
}

正确性: Dij算法每次收入的点是当前Q中(尚未被染色的)满足d最小的点u, u.d此时已经满足被所有S中能到v的点给更新了. 那么为什么不担心之后加入S的点可能还会再减小v.d呢? 这是因为之后再加S的点, 必须经过之前的S的点, 这些后加点的v.d只可能在已有能连到它的点的基础上递增, 因为Dij算法要求图中没有负权重边.

  • 时间复杂度分析:
    (1) 使用二维数组W存储weight, 一维数组V存储结点: 初始化操作Θ(V);Extra-Min操作V次, 每次耗时Θ(V), 所以这个环节是Θ(V^2); 对边的操作, 总共有E条边, 每次耗时都是Θ(1), 因此这个环节Θ(E); 总体耗时因此是Θ(V^2+E); (注意到V-1<=E<=V^2, 因此可以写成Θ(V^2)).
    (2) 使用二叉堆构建最小优先队列, Extract-Min耗时总共O(VlgV), 对边的操作需要做E次Decrease-Key, 耗时总共O(ElgV); 总体耗时因此为O((V+E)lgV), 因为E>=V-1总是成立, 因此可以写作O(ElgV);
    (3) 使用斐波那契堆实现最小优先队列Q的话, Extract-Min时间仍是lgV, 但是Decrease-Key下降到O(1), 因此总体上时间是O(VlgV+E), 小于O(ElgV).
    原文作者:陈码工
    原文地址: https://www.jianshu.com/p/65d399cb8f2f
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞