0
点赞
收藏
分享

微信扫一扫

带负权图最短路径之Bellman-Ford和SPFA——附模版伪代码、完整代码和示例

1 Bellman-Ford(简称BF算法)

  • 可以解决带负权图的最短路径问题
  • 时间复杂度O(VE)

1.1 模版

for (int i = 0; i < n - 1; ++i)//执行n-1次操作,n为顶点数
{
for (each edge u->v)//每轮操作都遍历所有边
{
if(d[u] + length[u->v] < d[v]){//以u为中介点可以使d[v]更小
d[v] = d[u] + length[u->v];//松弛操作
}
}

}

for (each edge u->v)//对每条边进行判断
{
if(d[u] + length[u->v] < d[v]){//如果仍可被松弛
return false;//说明图中有从源点可达的负环
}
return true;//数组d中所有值都已达到最优
}

  • 为什么要进行n-1次操作?
  • 1 最短路径树:如把源点s作为树的根结点,把其他结点按照最短路径的结点顺序连接,就会形成一颗最短路径树。
  • 2 原图和源点一旦确定,最短路径树就确定了。由于最短路径上的顶点数一定不超过V个,因此最短路径树的层数一定不超过V。
  • 3 由于初始状态d[s]为0,接下来的步骤d[s]都不变(最短路径树中第一层结点的d被确定),通过一轮Bellman-Ford算法,第二层顶点的d值也会被确定下来,然后进行第二轮Bellman-Ford算法,第三层顶点的d值也会被确定下来。这样计算直到最后一层顶点d值被确定。因此Bellman-Ford算法的松弛操作不会超过V-1轮。
  • 为什么最后要对所有边再进行Bellman-Ford操作?(判断是否存在从源点可达的负环)
  • 1 如下图所示,环A->B->C中的边权之和分别为0、正、负 。如果图中的负环,从源点出发可到达,就会影响最短路径的求解。
    带负权图最短路径之Bellman-Ford和SPFA——附模版伪代码、完整代码和示例_数组

1.2 实现代码

  • 由Bellman-Ford要遍历所有边,如果使用邻接矩阵,复杂度会上升到O(V3),因此使用Bellman-Ford算法。

1.2.1 朴素算法

struct node
{
int v;//邻接边的目标顶点
int dis;//邻接边的边权
};

const int MAXV = 1000;
const int INF = 0x3fffffff;
vector<node> G[MAXV];//图
int N;//顶点个数
int d[MAXV];//起点到达各顶点的最短路径

bool Bellman(int s){
fill(d, d + MAXV, INF);//将整个d数组赋值为INF
d[s] = 0;//起点到自身的距离为0
for (int i = 0; i < N - 1; ++i)//进行N-1轮操作
{
for (int u = 0; u < N; ++u)//每轮操作都遍历所有边
{
for (int j = 0; j < G[u].size(); ++j)
{
int v = G[u][j].v;//邻接边的顶点
int dis = G[u][j].dis;//邻接边的边权
if(d[u] + dis < d[v]){//以u为中介点可以使d[v]更小
d[v] = d[u] + dis;//松弛操作
}
}
}
}
//以下为判断负环
for (int u = 0; u < N; ++u)
{
for (int j = 0; j < G[u].size(); ++j)
{
if(d[u] + G[u][j].dis < d[G[u][j].v]){
return false;
}
}
}

return true;//数组中的所有值都已达到最优
}

1.2.2 算法优化

  • 如果某一轮操作时,发现所有边都没有被松弛,说明数组d中的所有值都已达到最优,不需要在继续,提前退出即可。

const int MAXV = 1000;
const int INF = 0x3fffffff;
vector<node> G[MAXV];//图
int N;//顶点个数
int d[MAXV];//起点到达各顶点的最短路径

bool Bellman(int s){
fill(d, d + MAXV, INF);//将整个d数组赋值为INF
d[s] = 0;//起点到自身的距离为0

for (int i = 0; i < N - 1; ++i)//进行N-1轮操作
{
bool flag = true;判断是否提前退出
for (int u = 0; u < N; ++u)//每轮操作都遍历所有边
{
for (int j = 0; j < G[u].size(); ++j)
{
int v = G[u][j].v;//邻接边的顶点
int dis = G[u][j].dis;//邻接边的边权
if(d[u] + dis < d[v]){//以u为中介点可以使d[v]更小
d[v] = d[u] + dis;//松弛操作
flag = false;数组d还未达到最优
}
}
}
if(flag == true){说明数组d中的所有值都已达到最优
break;
}
}
//以下为判断负环
for (int u = 0; u < N; ++u)
{
for (int j = 0; j < G[u].size(); ++j)
{
if(d[u] + G[u][j].dis < d[G[u][j].v]){
return false;
}
}
}

return true;//数组中的所有值都已达到最优
}

  • 由于Bellman-Ford算法会多次访问曾经访问过的顶点,因此统计最短路径条数时,需要设置前驱数组set<int>pre[MAXV],当遇到一条和已有最短路径相同的路径时,必须重新计算最短路径条数。详细例子见1.2
  • 其他如最短路径、有多重标尺时做法均和​相同

1.2 示例

​ 代码:

#include 
#include
#include
#include
#include

using std::vector;
using std::fill;
using std::set;

struct node
{
int v;
int dis;
node(int _v, int _dis) : v(_v), dis(_dis){}//构造函数
};

const int INF = 0x3fffffff;
const int MAXV = 510;
vector<node> G[MAXV];
int N;
int d[MAXV];//最短距离
int w[MAXV];//最大点权之和
int num[MAXV];//最短路径条数
int weight[MAXV];//点权
int start, end;
set<int> pre[MAXV];//前驱

void Bellman(int s){
fill(d, d + MAXV, INF);
memset(w, 0, sizeof(w));
memset(num, 0 , sizeof(num));

d[s] = 0;
w[s] = weight[s];
num[s] = 1;

for (int i = 0; i < N - 1; ++i)//执行n-1轮操作
{
for (int u = 0; u < N; ++u)//每轮遍历所有边
{
for (int j = 0; j < G[u].size(); ++j)
{
int v = G[u][j].v;//邻接边的顶点
int dis = G[u][j].dis;//邻接边的边权
if(d[u] + dis < d[v]){//以u为中介点时令d[v]更小
d[v] = dis + d[u];//覆盖d[u]
w[v] = w[u] + weight[v];
num[v] = num[u];
pre[v].clear();
pre[v].insert(u);
}else if(d[u] + dis == d[v]){//找到一条长度相同的路径
if(w[u] + weight[v] > w[v]){//以u为中介点时点权之和更大
w[v] = w[u] + weight[v];
}
pre[v].insert(u);//将u加入pre[v]
num[v] = 0;//重新统计num[v]
for (set<int>::const_iterator it = pre[v].begin(); it != pre[v].end() ; ++it)
{
num[v] += num[*it];
}
}
}
}
}
}

int main(int argc, char const *argv[])
{
int m;
scanf("%d%d%d%d", &N, &m, &start, &end);

for (int i = 0; i < N; ++i)
{
scanf("%d", &weight[i]);
}

int e1, e2, distance;
for (int i = 0; i < m; ++i)
{
scanf("%d%d%d", &e1, &e2, &distance);
G[e1].push_back(node(e2, distance));
G[e2].push_back(node(e1, distance));
}

Bellman(start);
printf("%d %d\n", num[end], w[end]);

return 0;
}

2 SPFA(Shortest Path Faster Algorithm)

  • 时间复杂度O(kE),k是一个常数,E是图的边数,很多情况k不超过2(如果遇到源点可达的负环,时间复杂度会退化到O(VE))
  • 如何从Bellman—Ford算法优化而得?
  • Bellman—Ford算法每轮都会操作所有边,显然这其中有大量无意义的操作。
  • 显然,只有当顶点u的d[u]值改变时,从它出发的边的邻接点v的d[v]值才有可能被改变
  • 优化:
  • 1 建立一个队列,每次将队首顶点u取出,然后对从u出发的所有u->v进行松弛操作,也就是判断d[u] + lengh[u->v] < d[v] 是否成立;
  • 2 如果成立,则用d[u] + lengh[u->v] 覆盖 d[v],与d[v]获得更优值,此时如果v不在队列,就把v加入队列;
  • 3 这样操作直到队列非空(说明图中没有从源点可达的负环),或是某个顶点的入队次数超过V-1(说明图中存在从源点可达的负环)


2.1 模版伪代码

queue<int> Q;
源点入队;
while(队列非空){
取出队首元素;
for (u的所有邻接边u->v)
{
if(d[u] + dis < d[v]){
d[v] = dis + d[u];
if(v不在队列){
v入队;
if(v入队大于n-1){
说明有可达负环,return;
}
}
}
}
}

  • 1 如果事先知道图中会不会有环,那么num数组的部分可以去掉。
  • 2 使用SPFA可以判读是否存在从源点可达的负环,如果负环从源点不可达,需要添加一个辅助顶点C,并添加一条从源点可达C的有向边以及V-1条从C到达除源点外各顶点的有向边才能判断负环是否存在。
  • 3 代码中的FIFO队列可以替换为优先队列(priority_queue),以加快速度;或者替换为双端队列(deque),使用使用SLF优化和LLL优化,以使效率提高至少50%。
  • 4 如果将队列换成栈,可以事先DFS版的SPFA,对判环有奇效。

struct node
{
int v;
int dis;
};

const int MAXV = 1000;
const int INF = 0x3fffffff;
vector<node> G[MAXV];
int N;
int d[MAXV];//记录最短距离
int num[MAXV];//记录顶点入队次数
bool inq[MAXV];

bool SPFA(int s){
//初始化部分
memset(inq, false, sizeof(inq));
memset(num, 0, sizeof(num));
fill(d, d + MAXV, INF);
//源点入队部分
queue<int> Q;
Q.push(s);
inq[s] = true;
num[s]++;//源点入队次数加1
d[s] = 0;
//主体部分
while(!Q.empty()){
int u = Q.front();//队首顶点编号u
Q.pop();//出队
inq[u] = false;//设置u为不在队列
//遍历u的所有邻接边v
for (int j = 0; j < G[u].size(); ++j)
{
int v = G[u][j].v;
int dis = G[u][j].dis;
//松弛操作
if(d[u] + dis < d[v]){
d[v] = d[u] + dis;
if(!inq[v]){//如果v不在队列中
Q.push(v);//v入队
inq[v] = true;//设置v在队列
num[v]++;//v入队次数加1
if(num[v] >= N) return false;
}
}
}
}
return false;
}

举报

相关推荐

0 条评论