图的单源最短路径问题的算法,速度较慢但可适用于所有图,事件复杂度 O(n*m)
所谓松弛操作是指如下代码
private relax(int v,int u){ if(dist[v] + edge[v][u] < dist[u]){ dist[u] = dist[v] + edge[v][u]; } }其中, dist 表示从源点到指定节点的最短路径,那么如何理解这段操作呢?
如上图所示,如果借助 v 到达 u 的距离比原先认为的最短距离要短,那自然可以借助 v 到达 u 从而更新最短距离
除此看见这个过程你可能感到非常不解,不妨先看看一个例子
以 1 为源点,初始化 dist 数组为
1234560 ∞ \infty ∞ ∞ \infty ∞ ∞ \infty ∞ ∞ \infty ∞ ∞ \infty ∞不妨手动的尝试一次对全体边集的松弛操作,此时将获得新的 dist 数组
1234560111 2 o r ∞ 2\ or\ \infty 2 or ∞ 2 o r ∞ 2\ or\ \infty 2 or ∞你可能注意到了, 5 和 6 号节点的 dist 值是不定的,事实上这取决于你访问边的顺序,当我们访问 edge[2][5] 时,无法确定 dist[2] 是 1 还是无穷,因为我们可能还没有访问 edge[1][2]
但我们可以确定的是,第一轮结束后,2,3,4 的值必然是确定的,然后让我们再次看看第二轮的结果
123456011122此时我们获得了最终的结果,可以看到哪怕我们没有进行 n - 1 次的遍历也获得了最终的结果
经过上述过程,可以观察到一个结果,经过 k 次边集的遍历,至少可以求出源点经过 1 到 k条边到达任意点的最短距离
怎么理解这句话呢?观察第一次遍历后的结果,对于 2,3,4 节点而言,我们求出了它经过 1 条边的最短距离为 1 ;而对于 5,6 节点而言,如果我们求出了 ∞ \infty ∞ 则我们求出的是经过了 1 条边到达的结果,显然是不可达的,而如果求出了 2 2 2,则是提前求出了经过两条边可以到达的路径长度,这也正是“至少”的含义
而到了第二次遍历后,我们一定能求出 5,6 的值为 2,因为我们一定会求出它在经过 1 或 2 条边到达的最小距离
就此,我们正式明白了遍历一次边集的含义,因而为何要遍历 n - 1 次就不言而喻了。在没有负边及负环的情况下,源点到某点的最短路径至多包含 n 个节点,n-1条边
时间复杂度 O(nm) n 为点数,m为边数
前面的算法本质上求的是在经过有限边数内到达任意点的最小距离,在没有负边的前提下,这就是全局的最小距离,而在有负边的情况下,则有可能产生无穷小最小距离的情况
观察上图,未标识的边皆为权重1,我们不难发现,对于 4,5 两个节点来说,他们只需要反复的游走于红色的环路,就可以不断的使得 dist 减少,这种环路就是所谓的 负环
所有负环内的节点都必然有 dist 值为负无穷,且这些节点所能够到达的节点也是如此
因此要兼容存在负环的图,我们就需要找到上述节点,并将其dist值定义为负无穷
方法也不难,再次不断的遍历边并松弛,对每一个出现 dist 值再次减少的节点标记其 dist 值为 负无穷,直到在一次遍历中没有找到可以再次减少 dist 值的节点,则说明所有节点都得到应有的值了
前面提到,算法有可能“超前完成任务”,因此不妨简单的测试算法是否超前的完成了任务,从而提前的退出
// 主体部分 for (int i = 0; i < n - 1; i++) { boolean flag = false; // 检测此轮遍历有没有最短距离的变化 for (int[] edge : edges) { flag = relax(edge, dist); } if (!flag) { return dist;// 如果没有变化则说明已经可以返回了 } }纵观整个算法过程,不难看出,其时间消耗中有大量时间浪费在了无意义的对边的检视,例如最开始的例子
在第一次遍历时,我们之所以有可能获得 4,5 节点无穷的结果,就是因为我们率先遍历了 edge[2][5] 等边,而事实上这条边的检视只有在遍历 edge[1][2] 之后才有意义,却因为我们先遍历了 edge[2][5] 而不得不第二次遍历边集,因此第一次遍历 1edge[2][5]1 是无意义的,换句话说,对边的遍历应当具有某种时序性,而由此诞生而来的,正是 SPFA 算法