本篇博客将考察各种最短路径问题。
    无权最短路径
    Dijkstra 算法
    具有负边值的图
    无圈图
    所有顶点对间的最短路径
    最短路径的例子–词梯游戏

输入是一个赋权图:与每条边 (vi, vj) 相联系的是穿越该边的开销(或称为值)ci,j 。一条路径v1v2……vN的值是
这叫作赋权路径长(weighted path length)。而无权路径长只是路径上的边数,即 N-1。

单源最短路径问题(Single-Source Shortest-Path Problem):

给定一个赋权图 G=(V, E) 和一个特定顶点 s 作为输入,找出从 s 到 G 中每一个其他顶点的最短赋权路径。

例如,在图1 中,从 v1 到 v6 的最短赋权路径的值为6,路径为 v1 -> v4 -> v7 -> v6 ;在这两个顶点间的最短无权路径长为2。

图1 一个有向图G

前面例子中没有负值边,图2 中的图就指出了负边可能产生的问题。从 v5 到 v4 的路径值为1,但通过循环 v5, v4, v2, v5, v4 存在一条更短的路径,它的值为-5,这个最短路径仍然可以通过循环达到任意小。这个循环叫作负值圈(negative-const cycle)。在没有负值圈时,从 s 到 s 的最短路径为0。

图2 带有负值圈的图

我们可能使用图建立航线或其他大规模运输线路的模型,并利用最短路径算法计算两点间的最佳路线。在这样的以及许多实际应用中,我们可能想要找出从一个顶点 s 到另一个顶点 t 的最短路径。 当前,还不存在找出从 s 到一个顶点的路径比找出从 s 到所有顶点路径更快得算法。

我们将考虑求解该问题 4种形态的算法。首先,要考虑无权最短路径问题,并指出如何以 O(|E|+|V|) 时间求解它。其次,如果假设没有负边,那么如何求解赋权最短路径问题,这个算法在使用一些合理的数据结构实现时的运行时间为 O(|E| log|V|)。如果图有负边,则提供一个简单解法,时间界为 O(|E|·|V|)。最后,将以线性时间解决无圈图这种特殊情形的赋值问题。

无权最短路径

图3 表示一个无权图G,使用某个顶点 s 作为输入参数,我们想要找出从 s 到所有其他顶点的最短路径。显然,这是赋权最短路径问题的特殊情形,因为可以为所有的边都赋以权1。

设我们选择 s 为 v3,此时可立即得到 s 到 v3 的最短路径长为0,将其标记,如图4 所示。

然后开始寻找所有从 s 出发距离为1 的顶点,这可以通过考查邻接到 s 的那些顶点找到,即 v1 和 v6 ,将它表示在图5 中;然后找出从 s 出发最短路径恰为2 的顶点,找出所有邻接到 v1 和 v6 的顶点,这次搜索告诉我们,到 v2 和 v4 的最短路径长为2。

最后,通过考查那些邻接到刚被赋值的 v2 和 v4 的顶点可以发现, v5 和 v7 各有一条三边的最短路径,现在所有的顶点都已经被计算,如图7 所示。

这种搜索图的方法就是广度优先搜索(breadth-first search)。该方法按层处理顶点:距开始点最近的那些顶点首先被求值,而最远的那些顶点最后被求值,这很像树的层序遍历(level-order traversal)。

图8 显示出该算法要用到的表的初始配置,记录了该算法的进行过程。对于每个顶点,我们跟踪3 条信息。首先,把从 s 开始到顶点的距离放到 dv 栏中,开始时除 s 外所有的顶点都不可达。pv 栏中的项为簿记变量,它将使我们显示出实际的路径。known 栏中的项在顶点被处理后置为 true。当一个顶点被标记为 known 时,我们就有了不会再找到更便宜的路径的保证,因此对该顶点的处理实质上已经完成。

无权最短路径算法的伪代码

void Graph::unweighted(Vertex s)
{for each Vertex v{v.dist = INFINITY;v.known = false;}s.dist = 0;for(int currDist=0;currDist<NUM_VERTICES;currDist++)for each Vertex vif (!v.known && v.dist == currDist){v.known = true;for each Vertex w adjacent to vif (w.dist == INFINITY){w.dist = currDist + 1;w.path = v;}}
}

由于双层嵌套的 for 循环,因此该算法的运行时间为 O(|V|2)。一个明显的低效之处在于,尽管所有的顶点早已成为 known 了,但外层循环还是要继续,直到 NUM_VERTICES -1 为止。我们可以用类似于对拓扑排序所做的那样来排除这种低效性。在任一时刻,只存在两种类型其 dv ≠ ∞ 的 unknown 顶点。一些顶点的 dv =currDist,而其余的则有 dv = currDist + 1。这种想法可以通过使用一个队列而被进一步精化。其中数据变化如图9 所示。

图9 无权最短路算法期间数据变化

相关伪代码及其说明如下。

void Graph::unweighted(Vertex s)
{Queue<Vertex> q;for each Vertex vv.dist = INFINITY;s.dist = 0;q.enqueue(s); //初始时队列只含距离为currDist的顶点while (!q.isEmpty()){Vertex v = q.dequeue();for each Vertex w adjacent to vif (w.dist == INFINITY){w.dist = v.dist + 1;w.path = v;q.enque(w); //距离为currDist+1的邻接顶点自队尾入队}}
}

Dijkstra算法

解决单源最短路径问题的一般方法叫作 Dijkstra 算法。这个有30 年的历史的解法是贪婪算法最好的实例。贪婪算法一般分阶段求解一个问题,在每个阶段它都把出现的当作是最好的去处理。

Dijkstra 算法按阶段进行,正像无权最短路径算法一样。在每个阶段,Dijkstra 算法选择一个顶点 v,它在所有 unknown 顶点中具有最小的 dv,同时算法声明从 s 到 v 的最短路径是 known 的。阶段的其余部分由 dw 值的更新工作组成。

对于图1 中的例子,图10 表示初始配置,这里假设开始节点为 v1。第一个选择的顶点是 v1,路径的长为0。该顶点标记为 known,那么某些表项就需要调整。邻接到 v1 的顶点是 v2 和 v4,这两个顶点的项得到调整,如图11 所示。

下一步,选取 v4 并标记为 known。顶点 v3,v5,v6,v7 是邻接的顶点,都需要调整,如图12 所示。

接着选择 v2 。v4 已经是 known 的了,v5 是邻接的点但不做调整,因为经过 v2 的值为 2+10=13 大于已知的路径。然后选择 v5,v3,对 v6 的距离下调到 3+5=8。

再下一个选取的顶点为 v7:v6 下调到 5+1=6,如图15 所示。
最后,选择 v6,算法到此结束。

算法例程

下面给出实现 Dijkstra 算法的伪代码。每个 Vertex 存储算法中使用的各种数据成员

/**
* Vertex结构的伪代码描述
* 以实际的C++表示,路径通常为 Vertex* 型
* 而描述的许多代码段,或者要求解引用操作符 *,或者使用 -> 操作符,
* 而不用 . 操作符
* 这有点不利于对基本算法思路的理解
*/
struct Vertex
{List       adj;    //邻接list(表)bool       known;  DistType    dist;   //DistType可能是int型量Vertex        path;   //如上所述,很可能是 Vertex* 型//其他数据成员和成员函数视需要而定
};

通过反证法的证明可指出,只要没有边的值为负值,该算法总能够正常工作。如果使用顺序扫描顶点以找出最小值 dv 这种明显的算法,那么每一步将花费 O(|V|) 时间找到最小值,从而整个算法查找最小值将花费 O(|V|2) 时间。每次更新 dw 的时间是常数,总计为 O(|E|)。因此,总得运行时间为 O(|E|+|V|2) = O(|V|2)

void Graph::dijkstra(Vertex s)
{for each Vertex v{v.dist = INFINITY;v.known = false;}s.dist = 0;while (there is an unknown distance vertex){Vertex v = smallest unknown distance vertex;v.known = true;for each Vertex w adjacent to vif (!w.known){DistTypw cvw = cost of edge from v to w;if (v.dist + cvw < w.dist){//更新 wdecrease(w.dist to v.dist + cvw);w.path = v;}}}
}

如果图是稀疏的,边数 |E| = O(|V|),那么这种算法就太慢了,此时距离就需要存储在优先队列中进行处理。

打印路径

下面的递归例程可以打印出路径。该例程递归地打印路径上直到顶点 v 前面的顶点的路径,然后再打印顶点 v。

/**
* 假设到 v 的最短路径存在
* 在运行 Dijkstra算法之后打印该最短路径
*/
void Graph::printPath(Vertex v)
{if (v.path != NOT_A_VERTEX){printPath(v.path);cout << " to ";}cout << v;
}

具有负边值的图

如果图有负的边值,那么 Dijkstra 算法是行不通的。问题在于,一旦一个顶点 u 被声明是 known 的,那就可能从某个另外的 unknown 顶点 v 有一条回到 u 的很负的路径。在这种情况下,选取从 s 到 v 再回到 u 的路径要比从 s 到 u 但不过 v 更好。把赋权的算法和无权的算法结合可以解决这个问题,但要付出运行时间剧烈增长的代价。

开始,我们把 s 放到队列中。然后,在每一阶段我们让一个顶点 v 出队。找出所有邻接到 v 使得 dw > dv + cv,w 的顶点 w。然后更新 dw 和 pw,并在 w 不在队列中的时候把它放入队列中。可以为每个顶点设置一个比特位(bit) 以指示它在队列中出现与否。重复这个过程知道队列空为止。

带有负的边值的赋权最短路径算法的伪代码
void Graph::weightedNegative(Vertex s)
{Queue<Vertex>q;for each Vertex vv.dist = INFINITY;s.dist = 0;q.enqueue(s);while (!q.isEmpty()){Vertex v = q.dequeue();for each Vertex w adjacent to vif (v.dist + cvw < w.dist){//更新 ww.dist = v.dist + cvw;w.path = v;if (w is not already in q)q.enqueue(w);}}
}

每个顶点最多可以出队 |V| 次,因此,如果使用邻接表,则运行时间是 O(|E|·|V|)

无圈图

如果知道图是无圈的,则可以通过改变声明顶点为 known 的顺序,或叫作顶点选取法则,来改进 Dijkstra 算法。新法则是以拓扑顺序选择顶点的,因为当一个顶点 v 被选取后,按照拓扑排序的法则它没有从 unknown 顶点出发的入边,因此它的距离 dv 可不再被降低。由于选择和更新可以在拓扑排序实施的时候进行,因此算法能够一趟完成。

使用这种选取法则不需要优先队列,由于选择花费常数时间,因此运行时间为 O(|E|+|V|)

关键路径分析

无圈图的一个更重要的用途是关键路径分析法(critical path analysis)。

动作节点图

以图17 为例子。每个节点表示一个必须执行的动作以及完成动作所花费的时间。因此,改图叫作动作节点图(activity-node graph)。图中的边代表优先关系:一条边 (v, w) 意味着动作 v 必须在动作 w 开始前完成。

图17 动作节点图

这种类型的图常常用来模拟方案的构建。则需要考虑的重要问题为方案最早完成时间是何时?从图中可以看到,沿路径 A, C, F, H 需要10 个时间单位。另一个重要问题是确定哪些动作可以延迟,延迟多长,而不至于影响最少完成时间。例如,延迟 A, C, F ,H 中的任一个都将使完成时间推迟到10 个单位时间之后,而动作B 可以被延迟两个时间单位而不至于影响最后完成时间。

事件节点图

为了进行这种运算,我们把动作节点图转化成事件节点图(event-node graph)。每个事件对应一个动作和所有相关的动作的完成。从事件节点图中节点 v 可达到的那些事件只可在事件v 完成后才能开始。在一个动作依赖于多个其他动作的情况下,可能需要插入哑边(dummy edge) 和 哑结点(dummy node)。对应图17 的事件节点图如图18 所示。

图18 事件节点图

最早完成时间

为找出方案的最早完成时间,我们只需要找出从第一个事件到最后一个事件的最长路径的长。

如果 ECi 是节点 i 的最早完成时间,则可用的法则为

EC1 = 0
EC w = max(ECv + cv,w),

图19 显示了在本例中的事件节点图中每个事件的最早完成时间。

图19 最早完成时间

最晚时间

还可以计算每个事件能够完成而又不影响最后完成时间的最晚时间 LCi。进行这项工作的公式为

LCn = ECn
LCv = min(LCw - cv,w),

对于每个顶点,通过保存一个所有邻接而且在先的顶点的表,这些值就可以以线性时间算出。各顶点的最早完成时间通过顶点的拓扑排序算出,而最晚完成时间则通过倒转它们的拓扑顺序来计算。最晚完成时间如图20 所示。

图20 最晚完成时间

松弛时间

事件节点图中每条边的松弛时间(slack time) 代表对应动作可以被延迟而又不至于推迟整体完成的时间量。容易看出

Slack(v,w) = LCw - ECv - cv,w

图21 指出在事件节点图中每个动作的松弛时间(作为第三项被标示)。对于每个节点,其项上的数字是最早完成时间,而底下的数字是最晚完成时间。

图21 最早完成时间、最晚完成时间和松弛时间

某些动作的松弛时间为零,这些动作是关键性动作,它们必须按计划结束。至少存在一条完全由零-松弛边组成的路径,这样的路径就是关键路径(critical path)。本例中的关键路径则为 1, 2, 4, 7, 10。

所有顶点对间的最短路径

有时需要找出图中所有顶点之间的最短路径,这可以运行 |V| 次适当的单源(single-source) 算法,但对于稠密图,还是应该用更快些的算法。
之后会给出 对赋权图求解这种问题的一个 O(|V|3) 算法 。虽然对于稠密图它具有和运行 |V| 次简单(非-优先队列)Dijkstra 算法相同的时间界,但它循环非常紧凑,以至于这种专业化的所有顶点对算法很可能在实践中更快。当然,对于稀疏图,更快的是运行 |V| 次用优先队列编码的 Dijkstra 算法。

最短路径的例

思考以下问题:使用C++ 来计算词梯游戏(word ladder)。在一个词梯中,每个单词均由其前面的单词改变一个字母而得到。例如,可以通过一系列单字母替换而将 zero 转换为 five:zero hero here hire five。

这是一个无权最短路径问题,其中每一个单词都是一个顶点,如果两个单词可以通过单字母替换而相互转换,那么它们之间就有边存在(双向)。

该例程创建一个map,其关键字是单词,相应的值是包含从单字母变换得到的那些单词的vector 对象。即,这个 map 代表一个以邻接表格式表示的图。

//求词梯的C++ 例程
//从邻接映射(adjacency map) 进行最短路径计算,返回一个向量
//该向量包含从first 到second 得到的单词相继变化
unordered_map<string,string>
findChain(const unordered_map<string, vector<string>>& adjacentWords,const string& first, const string& second)
{unordered_map<string, string> previousWord;queue<string>q;q.push(first);while (!q.empty()){string current = q.front();q.pop();auto itr = adjacentWords.find(current);const vector<string>& adj = itr->second;for(const string&str:adj)if (previousWord[str] == ""){previousWord[str] = current;q.push(str);}}previousWord[first] = "";return previousWord;
}//在最短路径计算运行之后,计算包含从first 到second 得到的
//单词相继变化的vector 对象
vector<string> getChainFromPreviousMap(const unordered_map<string, string>& previous, const string& second)
{vector<string>result;auto& prev = const_cast<unordered_map<string, string>&>(previous);for (string current = second; current != ""; current = prev[current])result.push_back(current);reverse(begin(result), end(result));return result;
}

创作不易,如果这篇【文章】有帮助到你,希望可以给作者点个赞

【图论算法】最短路径算法(无权最短路径、Dijkstra算法、带负边值的图、无圈图)相关推荐

  1. 图论算法(三)--最短路径 的Bellman-Flod [ 带负权值图 ] 的解法(JAVA )

    Bellman-Flod算法 对于带有负权值的图,我们已经不能通过Dijkstra算法进行求解了 原因:Dijkstra每次都会找一个距源点(设为s)最近的点,然后将该距离定为这个点到源点的最短路径: ...

  2. 基于dijsktra算法的最短路径求解_基于dijkstra算法的AGV路径规划(含C++代码)

    文字&代码来源: @Wasabi喵喵喵 基于dijkstra算法的AGV路径规划 dijkstra算法的路径规划 经典Dijkstra算法是一种贪心算法,根据路径长度递增次序找到最短路径,通常 ...

  3. matlab求任意点最短路径,【最短路】求两点间最短路径的改进的Dijkstra算法及其matlab实现...

    代码来源:<图论算法及其matlab实现>(北京航空航天出版社) P18 书中提出了基于经典Dijkstra算法改进的两种算法. 其中算法Ⅱ的效率较高. 代码如下: 1 function ...

  4. 最短路径 - 迪杰斯特拉(Dijkstra)算法

    对于网图来说,最短路径,是指两顶点之间经过的边上权值之和最少的路径,并且我们称路径上的第一个顶点为源点,最后一个顶点为终点.最短路径的算法主要有迪杰斯特拉(Dijkstra)算法和弗洛伊德(Floyd ...

  5. python最短路径例子_[python]dijkstra 算法的 加权的最短路径 案例

    这个还有很多概念有点明确 from collections import defaultdict # defaultdict, 找不到的key的value 就设定为 0,https://blog.cs ...

  6. 图的最短路径算法及matlab实现(Dijkstra算法、Floyd算法、Bellman-Ford算法、Johnson 算法)

    图的最短路径算法 Dijkstra算法 Dijkstra算法研究的是从初始点到其他任一结点的最短路径,即单源最短路径问题,其对图的要求是不存在负权值的边. Dijkstra算法主要特点是以起始点为中心 ...

  7. python求最短路径的方法_python --Dijkstra 算法求取最短路径

    #大名鼎鼎的Dijkstra是一种广度优先算法: #!/usr/bin/env python3 # -*- coding: utf-8 -*- import numbers import numpy ...

  8. 图论算法》关于SPFA和Dijkstra算法的两三事

    本来我是想把这两个算法分开写描述的,但是SPFA其实就是Dijkstra的稀疏图优化,所以其实代码差不多,所以就放在一起写了. 因为SPFA是Dijkstra的优化,所以我想来讲讲Dijkstra. ...

  9. ios笔试题算法_微软笔试题-Dijkstra算法

    Dijkstra算法是典型的算法.Dijkstra算法是很有代表性的算法.Dijkstra一般的表述通常有两种方式,一种用永久和临时标号方式,一种是用OPEN, CLOSE表的方式,这里均采用永久和临 ...

最新文章

  1. 月份加日期前面用on还是in_表示时间或地点:in,on,at 的使用
  2. python上海培训哪里比较好-上海十大python培训机构排名
  3. Gentoo 安装日记 19 (安装系统日志和cron守护进程)
  4. python为什么这么火知乎_没想到 Google 排名第一的编程语言,为什么会这么火?...
  5. cocos2d-x之读取plist文件
  6. C++函数分文件编写
  7. CButton相关函数介绍
  8. 在 Mac App Store 上如何查看未完成的下载?
  9. pojCashier Employment
  10. VB.NET连接SQL数据库
  11. 北大青鸟python教程_北大青鸟python课程六大优势
  12. oracle 11g jdk设置,java-如何从oracle 11g jdbc 7/14 jdk 1.7中的读取调...
  13. windows_server_2008_远程桌面(授权、普通用户登录多用户登录
  14. Beyond Compare反编译插件提示转换错误
  15. pytorch tensor 初始化_Pytorch - nn.init 参数初始化方法
  16. java.sql.SQLException: Undefined Error
  17. (转)coures包下载和安装 可解决报错ImportError: No module named '_curses'
  18. CSS3实现无限循环的无缝滚动
  19. CSS-使用background实现四个角边框
  20. 计算机主板属于什么垃圾分类类别,充电宝属于什么垃圾?废弃充电宝如何垃圾分类...

热门文章

  1. 一个CUE文件解析类
  2. 大象起舞,“TME思路”释放中国音乐全球影响力
  3. windows下解密微信数据库
  4. RPD出品:Superpower Squad 保姆级攻略
  5. 蒲公英上传应用后推送消息到钉钉、微信、QQ
  6. [18调剂]湖南工业大学2018年接收硕士研究生调剂公告
  7. Java FileInputStream类
  8. 【Linux应用】串口UART编程
  9. 打印Thread六种状态的例子
  10. ios 设计模式备忘录(1)