一、课程目标
- 负权图
- Bellman-Ford算法
- SPFA算法
二、目标详解
1、负权图
1.1 dijkstra对负权的问题
图中含有负权的边,此时用dijkstra算法求不出最短路,因为贪心法则不成立。
例如:假设U集合有{d[mn], d[a], d[b]}:
- 最小值d[mn]被加入到S集合,并且用mn松弛其邻接边
- 假设d[x]被松弛,于是U集合变为{d[a], d[b], d[x]}
- 之后,如果d[x] < d[a]、d[b],则d[x]也加入S集合(s->x的最短路)
这个过程忽略掉了a、b点可能对x的松弛,因为在g[a][x]为正权的前提下,肯定有d[x] < d[a] < d[a] + g[a][x],所以dijkstra算法节约了大量的无效松弛过程。
然而,如果g[a][x]为负数,以上不等式d[x] < d[a] < d[a] + g[a][x]就不一定成立了,那么就需要继续松弛比较,因此dijkstra算法对负权图无效。
1.2 持续松弛原理
对于负权图的某个点x,凡是有边连接到x的点,称为x的前趋点。
显然,需要所有的前趋点都对它松弛一遍,才能得到最短的d[x]。(最小值模式)
假设x的一个前趋点为y,而 y也有前趋点z……。
问题
来了,y松弛x的时候,用的是d[y]+w(y, x),但此时z尚未松弛y,因此d[y]可能后面还会变小。于是,后续还要继续用y对x进行松弛。
基于这个思路,出现了两个算法:Bellman-Ford与SPFA。
3、Bellman-Ford算法
从源点s出发,将所有边排序,依次用每个边的起点松弛边的终点,这个过程称为一遍松弛。
算法原理
:每个顶点理论上可能被任何前趋顶点松弛,一遍松弛不够(其前趋点可能尚未被松弛),就用多遍松弛,极端情况下最多n-1遍(假设某前趋点的最短距离经过了所有的顶点)。
判定完成
:
- 如果某一遍松弛过程,发现每个距离都已经是最短距离,则判定完成。
- 算法通过一个flag标记初始为false,当有距离被更新为更小时设置为true,如果一遍松弛下来,发现这个flag为false,则判定完成。
- 实际上,有负环存在则无法计算最短路,因此该flag也可用作判定负环是否存在。
算法设计
:
- n-1次循环:每次遍历每条边,用起点对终点进行松弛。
- 每次通过flag判定是否完成,如完成就提前结束,表示没有负环。
- 再做一遍松弛,判定是否完成。如果还可以松弛,表示肯定有负环。
伪代码
:
bool relax() {
bool flag = true;
for(所有边)
if(可以松弛) {
更新最短距离
flag = false;
}
reutrn flag;
}
bool bellman_ford(int v0) {
初始化d[N]和d[v0];
for(n-1次)
if(relax(i))
return true;
return relax();
}
复杂度
:
- Bellman-Ford的时间复杂度为O(VE),效率不高。
- 优点是支持负权、实现简单,并可以用来找负环。
3、SPFA算法
在Bellman-Ford算法中,每次都用边的起点x对终点y进行松弛,如果该边上一轮已经松弛过,并且之后起点x没有被松弛(d[x]没变小),则有d[x]+w == d[y],所以x是无需松弛的,做了无效运算。
SPFA算法基于这点进行优化,只有被松弛过的点(既d[x]被变小过)才能再执行松弛,从而减少了大量无效的运算。
SPFA算法为Bellman-Ford的一种优化算法,论文时间复杂度为O(kE)(k为小常数,表示顶点平均参与松弛次数),在稠密图上,复杂度接近Bellman-Ford算法的复杂度O(VE)。
SPFA算法一般使用队列来实现(类似BFS的思路,区别在于顶点可重复入队):
void spfa(s) {
初始化d[N]=oo、vis[N]=false、d[s]=0, vis[s]=true;
s点入队;
while(队列不空) {
出队;
清除入队标记;
for(邻接点) {
if(松弛) {
更新最短距离;
if(已入队) continue;
入队;
记录入队状态;
}
}
}
}
判定负环
:
- 正常来说,任意一个最短路径经过的点最多n个,既包含n个顶点。
- 用cnt[i]表示起点到顶点i的最短路径经过的点数,显然,如果cnt[i]>n则有负环
- 当x松弛y有效(更新)时,设cnt[y] = cnt[v] + 1