一、Bellman-Ford
Bellman-Ford 算法是一种用于计算带权有向图中单源最短路径(当然也可以是无向图)。与Dijkstra相比的优点是,也适合存在负权的图。
若存在最短路(不含负环时),可用Bellman-Ford求出,若最短路不存在时,Bellman-Ford只能用来判断是否存在负环。
松弛:
每次松弛操作实际上是对相邻节点的访问(相当于广度优先搜索),第n次松弛操作保证了所有深度为n的路径最短。由于图的最短路径最长不会经过超过|V| - 1条边,所以可知贝尔曼-福特算法所得为最短路径,也可只时间复杂度为O(VE)。
负边权操作:
与迪科斯彻算法不同的是,迪科斯彻算法的基本操作“拓展”是在深度上寻路,而“松弛”操作则是在广度上寻路,这就确定了贝尔曼-福特算法可以对负边进行操作而不会影响结果。
负权环判定:
因为负权环可以无限制的降低总花费,所以如果发现第n次操作仍可降低花销,就一定存在负权环。
基本操作:
- 创建源顶点 v 到图中所有顶点的距离的集合 distSet,为图中的所有顶点指定一个距离值,初始均为 Infinite,源顶点距离为 0;
- 计算最短路径,执行 V - 1 次遍历;
- 对于图中的每条边:如果起点 u 的距离 d 加上边的权值 w 小于终点 v 的距离 d,则更新终点 v 的距离值 d;
- 检测图中是否有负权边形成了环,遍历图中的所有边,计算 u 至 v 的距离,如果对于 v 存在更小的距离,则说明存在环(无向图不能用这种方法判断负环)
正确性:
Bellman-Ford 算法采用动态规划进行设计,实现的时间复杂度为 O(V*E),其中 V 为顶点数量,E 为边的数量。简单的说我们用
dis[k][v]表示经过前i个顶点到达v的最短路,易得转移方程dis[k][v] = min(dis[k][v],dis[ k -1][u] + w)。未使用滚动数组优化空间时,实现的代码如下:
1 int dis[maxv][maxv]; //dis[k][v];表示选取前k个时到达i的最短距离 2 struct Edge 3 { 4 int u, v, w; 5 }edge[maxv]; 6 int n, m; 7 8 void Bellman_Ford(int s) 9 { 10 memset(dis, INF, sizeof(dis)); 11 for (int i = 1; i <= n; i++) dis[i][s] = 0; 12 for (int k = 1; k <= n - 1; k++) 13 for (int i = 0; i < m; i++) 14 { 15 int u = edge[i].u, v = edge[i].v, w = edge[i].w; 16 dis[k][v] = min(dis[k][v], dis[k - 1][u] + w); 17 } 18 }
优化:
循环的提前退出:
在实际操作中,贝尔曼-福特算法经常会在未达到 |V| - 1 次前就出解,|V| -1 其实是最大值。于是可以在循环中设置判定,在某次循环不再进行松弛时,直接退出循环,进行负权环判定。
队列优化:
即SPFA
二、SPFA
是一个用于求解有向带权图单源最短路径的改良的贝尔曼-福特算法(当然也可以通过将每条边换为两条逆向的边来用于无向图)。这一算法被认为在随机的稀疏图上表现出色,并且极其适合带有负边权的图。然而SPFA在最坏情况的时间复杂度与Bellman-Ford算法相同,因此在非负边权的图中仍然最好使用Dijkstra。
原理:
基于Bellman-Ford之外,再可以确定,松弛操作必定只会发生在最短路径前导节点松弛成功过的节点上,用一个队列记录松弛过的节点,可以避免了冗余计算。
优化:
SPFA算法的性能很大程度上取决于用于松弛其他节点的备选节点的顺序。我们注意到其与Dijkstra很像,一方面,优先队列替换成普通的FIFO队列,而另一方面,一个节可以多次进入队列点。
事实上,如果 q 是一个优先队列,则这个算法将极其类似于戴克斯特拉算法。然而尽管这一算法中并没有用到优先队列,仍有两种可用的技巧可以用来提升队列的质量,并且借此能够提高平均性能(但仍无法提高最坏情况下的性能)。两种技巧通过重新调整 q 中元素的顺序从而使得更靠近源点的节点能够被更早地处理。
距离小者优先(Small Lable First(SLF)):
将总是把v压入队列尾端改为比较dis[v]与dis[q.front()]的大小(为了避免出现队列为空的操作,先将v压入队尾),并且在v较小时将v压入队列的头端。
距离大者优先(Large Lable Last(LLL)):
我们更新队列以确保队列头端的节点的距离总小于平均,并且任何距离大于平均的节点都将被移到队列尾端。
改为DFS版:
dfs版spfa判环根据:若一个节点出现2次及以上,则存在负环。具有天然的优势。由于是负环,所以无需像一般的spfa一样初始化为极大的数,只需要初始化为0就够了(可以减少大量的搜索,但要注意最开始时for一遍)。