【编码】最短路径算法合集(二)Dijkstra算法和Bellman-ford算法

Dijkstra 方法

Dijkstra 方法依据其优先队列的实现不同,可以写成几种时间复杂度不同的算法。它是图论-最短路中最经典、常见的算法。关于这个方法,网上有许多分析,但是我最喜欢的还是《算法概论》中的讲解。为了理解 Dijkstra 方法,首先回顾一下无权最短路的算法。无权最短路算法基于 BFS,每次从源点向外扩展一层,并且给扩展到的顶点标明距离,这个距离就是最短路的长。我们完全可以仿照这个思路,把带权图最短路问题规约到无权图最短路问题——只要把长度大于 1 的边填充进一些「虚顶点」即可。如下图所示。

《【编码】最短路径算法合集(二)Dijkstra算法和Bellman-ford算法》

这个办法虽然可行,但是显然效率很低。不过,Dijkstra 方法EC,EB,EDEC,EB,ED分别出发,经过一系列「虚节点」,依次到达D,B,CD,B,C 。为了不在虚节点处浪费时间,出发之前,我们设定三个闹钟,时间分别为4,3,24,3,2提醒我们预计在这些时刻会有重要的事情发生(经过实际节点)。更一般地说,假设现在我们处理到了某个顶点uu,和uu相邻接的顶点为v1,v2,,vnv1,v2,…,vn,它们和uu的距离为d1,d2,,dnd1,d2,…,dn。我们为v1,v2,,vnv1,v2,…,vn各设定一个闹钟。如果还没有设定闹钟,那么设定为dd ;如果设定的时间比dd晚,那么重新设定为dd(此时我们沿着这条路比之前的某一条路会提前赶到)。每次闹钟响起,都说明可能经过了实际节点,我们都会更新这些信息,直到不存在任何闹钟。综上所述,也就是随着 BFS 的进行,我们一旦发现更近的路径,就立即更新路径长,直到处理完最后(最远)的一个顶点。由此可见,由于上述「虚顶点」并非我们关心的实际顶点,因此 Dijkstra 方法的处理方式为:直接跳过了它们。

还需要解决的一个问题,就是闹钟的管理。闹钟一定是从早到晚按顺序响起的,然而我们设闹钟的顺序却不一定按照时间升序,因此需要一个优先队列来管理。Dijkstra 方法实现的效率严重依赖于优先队列的实现。一个使用标准库容器适配器 priority_queue 的算法版本如下:

《【编码】最短路径算法合集(二)Dijkstra算法和Bellman-ford算法》

 1 typedef pair<int, int> HeapNode;
 2 void Dijkstra(int s)
 3 {
 4     priority_queue< HeapNode, vector<HeapNode>, greater<HeapNode> > Q;
 5     for (int i=0; i<num_nodes; ++i)
 6         d[i] = __inf;
 7 
 8     d[s] = 0;
 9     Q.push(make_pair(0, s));
10     while (!Q.empty()) {
11         pair<int, int> N = Q.top();
12         Q.pop();
13         int u = N.second;
14         if (N.first != d[u]) continue;
15         for (int i=0; i<G[u].size(); ++i) {
16             Edge &e = edges[G[u][i]];
17             if (d[e.to] > d[u] + e.weight) {
18                 d[e.to] = d[u] + e.weight;
19                 p[e.to] = G[u][i];
20                 Q.push(make_pair(d[e.to], e.to));
21             }
22         }
23     }
24 }

《【编码】最短路径算法合集(二)Dijkstra算法和Bellman-ford算法》

Bellman-Ford 算法描述:

1.创建源顶点 v 到图中所有顶点的距离的集合 distSet,为图中的所有顶点指定一个距离值,初始均为 Infinite,源顶点距离为 0;

2.计算最短路径,执行 V – 1 次遍历;

3.对于图中的每条边:如果起点 u 的距离 d 加上边的权值 w 小于终点 v 的距离 d,则更新终点 v 的距离值 d;

4.检测图中是否有负权边形成了环,遍历图中的所有边,计算 u 至 v 的距离,如果对于 v 存在更小的距离,则说明存在环;

注意:

上面的最后一步用于检测闭环的存在

步骤:

《【编码】最短路径算法合集(二)Dijkstra算法和Bellman-ford算法》

检测步骤:

《【编码】最短路径算法合集(二)Dijkstra算法和Bellman-ford算法》

Dijkstra 方法的本质是进行一系列如下的更新操作

然而,如果边权含有负值,那么 Dijkstra 方法将不再适用。原因解释如下。

假设最终的最短路径为:

不难看出,如果按照 (s, u1), (u1, u2), ,(uk, t)(s, u1), (u1, u2), …,(uk, t) 的顺序执行上述更新操作,最终tt的最短路径一定是正确的。而且,只要保证上述更新操作全部按顺序执行即可,并不要求上述更新操作是连续进行的。Dijkstra 算法所运行的更新序列是经过选择的,而选择基于这一假设:sts→t的最短路一定不会经过ss距离大于l(s, t)l(s, t)的点。对于正权图这一假设是显然的,对于负权图这一假设是错误的。

因此,为了求出负权图的最短路径,我们需要保证一个合理的更新序列。但是,我们并不知道最终的最短路径!因此一个简单的想法就是:更新所有的边,每条边都更新V1∣V∣−1次。由于多余的更新操作总是无害的,因此算法(几乎)可以正确运行。等等,为什么是V1∣V∣−1次?这是由于,任何含有V∣V∣个顶点的图两个点之间的最短路径最多含有V1∣V∣−1条边。这意味着最短路不会包含环。理由是,如果是负环,最短路不存在;如果是正环,去掉后变短;如果是零环,去掉后不变。

算法实现中唯一一个需要注意的问题就是负值圈 (negative-cost cycle)。负值圈指的是,权值总和为负的圈。如果存在这种圈,我们可以在里面滞留任意长而不断减小最短路径长,因此这种情况下最短路径可能是不存在的,可能使程序陷入无限循环。好在,本文介绍的几种算法都可以判断负值圈是否存在。对于 Bellman-Ford 算法来说,判断负值圈存在的方法是:在V1∣V∣−1次循环之后再执行一次循环,如果还有更新操作发生,则说明存在负值圈。

Bellman-Ford 算法的代码如下:

《【编码】最短路径算法合集(二)Dijkstra算法和Bellman-ford算法》

 1 bool Bellman_Ford(int s)
 2 {
 3     for (int i=0; i<num_nodes; ++i)
 4         d[i] = __inf;
 5 
 6     d[s] = 0;
 7     for (int i=0; i<num_nodes; ++i) {
 8         bool changed = false;
 9         for (int e=0; e<num_edges; ++e) {
10             if (d[edges[e].to] > d[edges[e].from] + edges[e].weight 
11                && d[edges[e].from] != __inf) {
12                 d[edges[e].to] = d[edges[e].from] + edges[e].weight;
13                 p[edges[e].to] = e;
14                 changed = true;
15             }
16         }
17         if (!changed) return true;
18         if (i == num_nodes && changed) return false;
19     }
20     return false; // 程序应该永远不会执行到这里
21 }

《【编码】最短路径算法合集(二)Dijkstra算法和Bellman-ford算法》

注记:

  1. 如果某次循环没有更新操作发生,以后也不会有了。我们可以就此结束程序,避免无效的计算。
  2. 上述程序中第 11 行的判断:如果去掉这个判断,只要图中存在负值圈函数就会返回 false。否则,仅在给定源点可以达到负值圈时才返回 false

(转自:https://www.cnblogs.com/ivancjw/p/6403665.html)

    原文作者:Bellman - ford算法
    原文地址: https://blog.csdn.net/weixin_39878297/article/details/80295909
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞