Bellman–Ford
简单介绍
Bellman-Ford算法与Dijkstra算法思想一样,用于求解单源点最短路径问题。
Bellman-ford算法除了可求解边权均非负的问题外,关键是还可以解决存在负权边的问题,而Dijkstra算法只能处理边权非负的问题,因此 Bellman-Ford算法的适用面要广泛一些。
但是,原始的Bellman-Ford算法时间复杂度为 O(VE),比Dijkstra算法的时间复杂度高,就连经典的《算法导论》也只介绍了基本的Bellman-Ford算法,事实上,有多种形式的Bellman-Ford算法的优化实现。这些优化实现在时间效率上得到相当提升,例如近一两年被热捧的SPFA(Shortest-Path Faster Algoithm 更快的最短路径算法)算法的时间效率甚至优于Dijkstra算法(除非极端情况)。
算法思想
贝尔曼-福特算法与迪科斯彻算法类似,都以松弛操作为基础,即估计的最短路径值渐渐地被更加准确的值替代,直至得到最优解。
在两个算法中,计算时每个边之间的估计距离值都比真实值大,并且被新找到路径的最小长度替代。 然而,迪科斯彻算法以贪心法选取未被处理的具有最小权值的节点,然后对其的出边进行松弛操作;而贝尔曼-福特算法简单地对所有边进行松弛操作,共|V | − 1次,其中 |V |是图的点的数量。在重复地计算中,已计算得到正确的距离的边的数量不断增加,直到所有边都计算得到了正确的路径。
这样的策略使得贝尔曼-福特算法比迪科斯彻算法适用于更多种类的输入。
贝尔曼-福特算法的最多运行O(|V|·|E|)次,|V|和|E|分别是节点和边的数量)。
下图表示算法的大概过程
在这个图中,假设A是起点,并且边以最坏的顺序处理,从右到左,需要|V|−1步或4次计算路径长度。相反地,若边以最优顺序处理,从左到右,算法只需要在一次遍历内完成。
算法流程
(1) 初始化:将除源点外的所有顶点的最短距离估计值 dist[v] ←+∞, dist[s] ←0;
(2) 迭代求解:反复对边集E中的每条边进行松弛操作,使得顶点集V中的每个顶点v的最短距离估计值逐步逼近其最短距离;(运行|v|-1次)
(3) 检验负权回路:判断边集E中的每一条边的两个端点是否收敛。如果存在未收敛的顶点,则算法返回false,表明问题无解;否则算法返回true,并且从源点可达的顶点v的最短距离保存在 dist[v]中。
伪代码表示
procedure BellmanFord(list vertices, list edges, vertex source)
for each vertex v
in vertices:
if v is source
then distance[v] :=
0
else distance[v] := infinity
predecessor[v] :=
null
for i
from 1 to size(vertices)-
1:
for each edge (u, v)
with weight w
in edges:
if distance[u] + w < distance[v]:
distance[v] := distance[u] + w
predecessor[v] := u
for each edge (u, v)
with weight w
in edges:
if distance[u] + w < distance[v]:
error
"图包含了负权环"
原理
松弛: 每次松弛操作实际上是对相邻节点的访问,第 {\displaystyle n} n次松弛操作保证了所有深度为n的路径最短。由于图的最短路径最长不会经过超过 V-1条边,所以可知贝尔曼-福特算法所得为最短路径。
负边权操作:与迪科斯彻算法不同的是,迪科斯彻算法的基本操作“拓展”是在深度上寻路,而“松弛”操作则是在广度上寻路,这就确定了贝尔曼-福特算法可以对负边进行操作而不会影响结果。
负权环判定:因为负权环可以无限制的降低总花费,所以如果发现第n次操作仍可降低花销,就一定存在负权环。
优化算法SFFA
适用范围
很多时候,给定的图存在负权边,这时类似Dijkstra等算法便没有了用武之地,而Bellman-Ford算法的复杂度又过高,SPFA算法便派上用场了。有人称spfa算法是最短路的万能算法。
简洁起见,我们约定有向加权图G不存在负权回路,即最短路径一定存在。当然,我们可以在执行该算法前做一次拓扑排序,以判断是否存在负权回路。
我们用数组dis记录每个结点的最短路径估计值,可以用邻接矩阵或邻接表来存储图G,推荐使用邻接表。
spfa的算法思想(动态逼近法):
设立一个先进先出的队列q用来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作,如果v点的最短路径估计值有所调整,且v点不在当前的队列中,就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止。
松弛操作的原理是著名的定理:“三角形两边之和大于第三边”,在信息学中我们叫它三角不等式。所谓对结点i,j进行松弛,就是判定是否dis[j]>dis[i]+w[i,j],如果该式成立则将dis[j]减小到dis[i]+w[i,j],否则不动。
下图为spfa算法实例
我们知道SPFA有两个很有名的优化 SLF 和 LLL SLF:Small Label First 策略。 实现方法是,设队首元素为 i,队列中要加入节点 j,在 dj<=di 时加到队首而不是队尾,否则和普通的 SPFA 一样加到队尾。 LLL:Large Label Last 策略。 实现方法是,设队列 Q 中的队首元素为 i,距离标号的平均值为 avg(d),每次出队时,若 di>avg(d),把 i 移到队列末尾,如此反复,直到找到一个 i 使 ,di<=avg(d)将其出队。
SPFA的两种写法,bfs和dfs,bfs判别负环不稳定,相当于限深度搜索,但是设置得好的话还是没问题的,dfs的话判断负环很快
DFS写法
int spfa_dfs(
int u)
{
vis[u]=
1;
for(
int k=f[u]; k!=
0; k=e[k].
next)
{
int v=e[k].v,w=e[k].w;
if( d[u]+w < d[v] )
{
d[v]=d[u]+w;
if(!vis[v])
{
if(spfa_dfs(v))
return 1;
}
else
return 1;
}
}
vis[u]=
0;
return 0;
}
BFS写法
int spfa_bfs(
int s)
{
queue <int> q;
memset(d,
0x3f,
sizeof(d));
d[s]=
0;
memset(c,
0,
sizeof(c));
memset(vis,
0,
sizeof(vis));
q.push(s); vis[s]=
1; c[s]=
1;
int OK=
1;
while(!q.empty())
{
int x;
x=q.front(); q.pop(); vis[x]=
0;
for(
int k=f[x]; k!=
0; k=nnext[k])
{
int y=v[k];
if( d[x]+w[k] < d[y])
{
d[y]=d[x]+w[k];
if(!vis[y])
{
vis[y]=
1;
c[y]++;
q.push(y);
if(c[y]>NN)
return OK=
0;
}
}
}
}
return OK;
}