图算法：Bellman-Ford算法和SPFA优化

Bellman Ford 算法介绍

Bellman Ford算法解决的是一般情况下的单源最短路径问题，不同于Dijkstra算法，Bellman Ford算法允许边的权重为负数。给定带权重的有向图G =（V, E）和权重函数 w:E>R ，Bellman Ford算法返回一个布尔值，以表明是否存在一个从源结点可以到达的权重为负值的环路，如果存在，算法将告诉我们不存在解决方案，如果不存在，算法将给出最短路径和他们的权重。

2. 简单的C++实现

#include<iostream>

using namespace std;

#define MAX 0x3f3f3f3f
#define N 1010

int nodeNum, edgeNum, original; // 点， 边， 起点

struct Edge {
int u, v;
int cost;
};

Edge edge[N];
int dis[N], pre[N];

bool Bellman_Ford() {
/* * 初始化 */
for(int i = 1; i <= nodeNum; ++i)
dis[i] = (i == orginal)? 0 : MAX;

/* * 松弛操作 */
for(int i = 1; i <= nodeNum - 1; ++i) {
for(int j = 1; j <= edgeNum; ++j) {
if(dis[edge[j].v] > dis[edge[j].u] + edge[j].cost) {
dis[edge[j].v] = dis[edge[j].u] + edge[j].cost;
pre[edge[j].v] = edge[j].u;
}
}
}
/* * 判断是否有含负权的回路 */
bool flag = true;
for(int i = 1; i <= edgeNum; ++i) {
if(dis[edge[i].v] > dis[edge[i].u] + edge[i].cost) {
flag = false;
break;
}
}
return flag;
}
/* * 打印出个各个点的最短路径 */
void print_shortestPath(int root) {
while(root != pre[root]) {
cout << root <<"->";
root = pre[root];
}
if(root == pre[root])
cout << root;
}

int main() {
cin << nodeNum << edgeNum << original;
pre[original] = original;

for(int i = 1; i<= edgeNum; ++i) {
cin >> edge[i].u >> edge[i].v >> edge[i].cost;
}
if(Bellman_Ford()) {
for(int i = 1; i <= nodeNum; ++i) {
cout << dis[i] << endl;
cout << "shortest path: ";
print_shortestPath(i);
}
} else {
cout << "There must be some negative circles" << endl;
}
return 0;
}

3. Bellman Ford算法的优化(SPFA，Shortest Path Faster Algorithm)

Shortest Path Faster Algorithm算法：

The Shortest Path Faster Algorithm (SPFA) is an improvement of the Bellman–Ford algorithm which computes single-source shortest paths in a weighted directed graph. The algorithm is believed to work well on random sparse graphs and is particularly suitable for graphs that contain negative-weight edges. However, the worst-case complexity of SPFA is the same as that of Bellman–Ford, so for graphs with nonnegative edge weights Dijkstra’s algorithm is preferred.The SPFA algorithm was published in 1994 by Fanding Duan. —— from WIKIPEDIA

伪代码
input G,v
for each u ∈ V(G)
let dist[u] = ∞
let dist[v] = 0
let Q be an initially empty queue
push(Q,v)
while not empty(Q)
let u = pop(Q)
for each (u,w) ∈ E(G)
if dist[w] > dist[u]+wt(u,w)
dist[w] = dist[u]+wt(u,w)
if w is not in Q
push(Q,w)

算法流程

SPFA——Shortest Path Faster Algorithm，它可以在O(kE)的时间复杂度内求出源点到其他所有点的最短路径，可以处理负边。SPFA的实现甚至比Dijkstra或者Bellman_Ford还要简单：设Dist代表S到I点的当前最短距离，Fa代表S到I的当前最短路径中I点之前的一个点的编号。开始时Dist全部为+∞，只有Dist[S]=0，Fa全部为0。

SPFA 在形式上和宽度优先搜索非常类似，不同的是宽度优先搜索中一个点出了队列就不可能重新进入队列，但是SPFA中一个点可能在出队列之后再次被放入队列，也就是一个点改进过其它的点之后，过了一段时间可能本身被改进，于是再次用来改进其它的点，这样反复迭代下去。设一个点用来作为迭代点对其它点进行改进的平均次数为k，有办法证明对于通常的情况，k在2左右。

SPFA算法（Shortest Path Faster Algorithm），也是求解单源最短路径问题的一种算法，用来解决：给定一个加权有向图G和源点s，对于图G中的任意一点v，求从s到v的最短路径。 SPFA算法是Bellman-Ford算法的一种队列实现，减少了不必要的冗余计算，他的基本算法和Bellman-Ford一样，并且用如下的方法改进： 1、第二步，不是枚举所有节点，而是通过队列来进行优化 设立一个先进先出的队列用来保存待优化的结点，优化时每次取出队首结点u，并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作，如果v点的最短路径估计值有所调整，且v点不在当前的队列中，就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作，直至队列空为止。 2、同时除了通过判断队列是否为空来结束循环，还可以通过下面的方法： 判断有无负环：如果某个点进入队列的次数超过V次则存在负环（SPFA无法处理带负环的图）。—— From nocow.

C++ 实现
/* * 单源最短路算法SPFA,时间复杂度O(kE),k在一般情况下不大于2,对于每个顶点使用可以在O(VE)的时间内算出每对节点之间的最短路 * 使用了队列,对于任意在队列中的点连着的点进行松弛,同时将不在队列中的连着的点入队,直到队空则算法结束,最短路求出 * SPFA是Bellman-Ford的优化版,可以处理有负权边的情况 * 对于负环,我们可以证明每个点入队次数不会超过V,所以我们可以记录每个点的入队次数,如果超过V则表示其出现负环,算法结束 * 由于要对点的每一条边进行枚举,故采用邻接表时时间复杂度为O(kE),采用矩阵时时间复杂度为O(kV^2) */
#include<cstdio>
#include<vector>
#include<queue>
#define MAXV 10000
#define INF 1000000000 //此处建议不要过大或过小,过大易导致运算时溢出,过小可能会被判定为真正的距离

using std::vector;
using std::queue;

struct Edge{
int v; //边权
int to; //连接的点
};

vector<Edge> e[MAXV]; //由于一般情况下E<<V*V,故在此选用了vector动态数组存储,也可以使用链表存储
int dist[MAXV]; //存储到原点0的距离,可以开二维数组存储每对节点之间的距离
int cnt[MAXV]; //记录入队次数,超过V则退出
queue<int> buff; //队列,用于存储在SPFA算法中的需要松弛的节点
bool done[MAXV]; //用于判断该节点是否已经在队列中
int V; //节点数
int E; //边数

bool spfa(const int st){ //返回值:TRUE为找到最短路返回,FALSE表示出现负环退出
for(int i=0;i<V;i++){ //初始化:将除了原点st的距离外的所有点到st的距离均赋上一个极大值
if(i==st){
dist[st]=0; //原点距离为0;
continue;
}
dist[i]=INF; //非原点距离无穷大
}
buff.push(st); //原点入队
done[st]=1; //标记原点已经入队
cnt[st]=1; //修改入队次数为1
while(!buff.empty()){ //队列非空,需要继续松弛
int tmp=buff.front(); //取出队首元素
for(int i=0;i<(int)e[tmp].size();i++){ //枚举该边连接的每一条边
Edge *t=&e[tmp][i]; //由于vector的寻址速度较慢,故在此进行一次优化
if(dist[tmp]+(*t).v<dist[(*t).to]){ //更改后距离更短,进行松弛操作
dist[(*t).to]=dist[tmp]+(*t).v; //更改边权值
if(!done[(*t).to]){ //没有入队,则将其入队
buff.push((*t).to); //将节点压入队列
done[(*t).to]=1; //标记节点已经入队
cnt[(*t).to]+=1; //节点入队次数自增
if(cnt[(*t).to]>V){ //已经超过V次,出现负环
while(!buff.empty())buff.pop(); //清空队列,释放内存
return false; //返回FALSE
}
}
}
}
buff.pop();//弹出队首节点
done[tmp]=0;//将队首节点标记为未入队
}
return true; //返回TRUE
} //算法结束

int main(){ //主函数
scanf("%d%d",&V,&E); //读入点数和边数
for(int i=0,x,y,l;i<E;i++){
scanf("%d%d%d",&x,&y,&l); //读入x,y,l表示从x->y有一条有向边长度为l
Edge tmp; //设置一个临时变量,以便存入vector
tmp.v=l; //设置边权
tmp.to=y; //设置连接节点
e[x].push_back(tmp); //将这条边压入x的表中
}
if(!spfa(0)){ //出现负环
printf("出现负环,最短路不存在\n");
}else{ //存在最短路
printf("节点0到节点%d的最短距离为%d",V-1,dist[V-1]);
}
return 0;
}
原文作者：Bellman - ford算法
原文地址: https://blog.csdn.net/w_bu_neng_ku/article/details/78158009
本文转自网络文章，转载此文章仅为分享知识，如有侵权，请联系博主进行删除。