Bellman-Ford算法(含队列优化)

Bellman-Ford算法

之前我已经介绍过Dijkstra算法,Dijkstra算法是个优秀的算法,但它不能运用到带有负权边的图中。
于是乎,Bellman-Ford算法登场了~

先上一张带有负权边的图( n = 5 n=5 n=5)作为例子,依旧是求顶点1到其他顶点的最短距离:
在这里插入图片描述

准备工作

我们用三个一维数组表示所有边,其中u[i],v[i],w[i]分别表示第i条边的起点、终点和权值,边给出的顺序如下:

在这里插入图片描述

另外,和Dijkstra算法类似,我们还需要一个dis数组存储顶点1到其他顶点的距离:

初始化

在这里插入图片描述

核心步骤

对于每一条边进行一次松弛操作。 对于第i条边 u [ i ] → w [ i ] v [ i ] u[i]\xrightarrow{w[i]}v[i] u[i]w[i] v[i] ,更新dis[v[i]]=min(dis[u[i]]+w[i])
例如,根据边给出的顺序,我们先来处理第一条边 2 → 2 3 2\xrightarrow{2}3 22 3 ,判断dis[2]+2是否大于dis[3],发现dis[3]=∞dis[2]+2=∞,松弛失败。
继续处理第2条边, 1 → − 3 2 1\xrightarrow{-3}2 13 2,判断dis[1]+(-3)是否大于dis[2],发现dis[2]=∞dis[1]+(-3)=-3,则dis[2]的值更新为-3,松弛成功。
用同样的方法处理剩下的边,dis数组最后如下:
在这里插入图片描述

经过一次上述步骤,dis数组存储的是从顶点1经过一条边后到达各个顶点的最短距离。实际上,经过k次上述步骤,dis数组存储的是从顶点1经过k条边后到达各个顶点的最短距离。

那么问题来了:到底要经过几轮该步骤呢?

最多经过n-1轮该步骤即可,原因如下:

  • 在一个含有n个顶点的图中,任意两点之间的最短路径最多包含n-1条边,因为n条边和n个顶点组成的图一定包含回路。
  • 最短路径一定是一个不包含回路的简单路径。回路分为正权回路(回路权值和为正)和负权回路。若最短路径中包含正权回路,那去掉这个回路就能得到更短路径,违背前提;若最短路径中包含负权回路,那么每走一次负权回路,路径的权值和就会减小,也就是说每次都能获得一个更短的路径,那图中就不存在最短路径了,依旧违背前提。

这样一来,Bellman-Ford算法可以概括成一句话: 对 于 每 一 条 边 进 行 n − 1 次 松 弛 操 作 \color{red}{对于每一条边进行n-1次松弛操作} n1。核心代码三行,这简洁度相比Floyd算法还更胜一筹~

for (int k = 1; k <= n - 1; k++) //n-1轮松弛
		for (int i = 1; i <= m; i++) //枚举m条边
			dis[v[i]] = min(dis[v[i]], dis[u[i]] + w[i]);//尝试松弛

利用这一性质,我们还可以判断图中是否存在负权回路,要做的就是判断n-1轮松弛之后dis数组是否还会变化,若依旧会发生变化,则必然存在负权回路。

优化

我在前面描述到,最多经过n-1轮的松弛即可,那为什么我用的是最多呢?因为在实际的运用场景中,得出最终的最短路数组dis往往不需要n-1轮的松弛,导致剩下几轮的松弛会变成无意义的循环,浪费时间。

所以我们可以对算法进行优化:每一轮松弛后判断dis是否发生变化,没有变化则已找到最短路径解,结束算法。

这里贴出优化后的完整代码:

#include <iostream>
#include <algorithm>
#include <queue>
#include <stack>
#include <stdio.h>
#include <limits.h>
#include <cmath>
#include <stdlib.h>
#include <string>
#include <vector>
#include <set>
#include <map>
#include <utility>
#include <windows.h>
#include <climits>

#define LL long long
#define INF 999999

using namespace std;

const double pi = atan(1.) * 4.;
const int N = 51;
const int MOD = 1e9 + 7;

int n, m;
int u[N], v[N], w[N];
int pdis[N];//前一轮松弛后dis数组的备份
int dis[N];
bool check = true;//判断算法结束的标志位

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);

	//准备工作-----------------------------------
	//输入顶点数、边数
	cin >> n >> m;

	for (int i = 1; i <= m; i++) {
		// 第i条边的起点、终点、权值
		cin >> u[i] >> v[i] >> w[i];
	}

	//初始化dis数组
	for (int i = 1; i <= n; i++) {
		dis[i] = INF;
	}
	dis[1] = 0;

	//核心步骤-----------------------------------
	for (int k = 1; k <= n - 1; k++) {
		for (int i = 1; i <= n; i++) //备份dis数组
			pdis[i] = dis[i];
		for (int i = 1; i <= m; i++) //枚举m条边
			dis[v[i]] = min(dis[v[i]], dis[u[i]] + w[i]);//尝试松弛
		for (int i = 1; i <= n; i++) //判断dis数组是否变化
			if (pdis[i] != dis[i]) {
				check = false;
				break;
			}
		if (check) break;//无变化则结束算法
	}
	
	//输出结果-----------------------------------
	for (int i = 1; i <= n; i++) cout << dis[i] << ' ';
}
/*
input:
5 5
2 3 2
1 2 -3
1 5 5
4 5 2
3 4 3

output:
0 -3 -1 2 4
*/

优化++

实际上再深入思考一下,我们会发现,在每一次松弛操作之后,有一些顶点已经求得最短路径,此后这些顶点的dis[i]就再也没有变过,但是每轮都还要判断是否需要松弛,这里也浪费了时间。

所以,我们可以对算法进行进一步的优化:每次仅对最短路发生变化的顶点的所有出边进行松弛操作

为了方便遍历顶点的出边,将图的存储形式改为邻接表,添加一个队列(这里用一维数组que来模拟)来维护这些点,最初将顶点1入队:
在这里插入图片描述
此时位于队首的是顶点1,来看它的第一个出边 1 → − 3 2 1\xrightarrow{-3}2 13 2,发现dis[2]=∞dis[1]+(-3)=-3,将dis[2]更新为-2,顶点2不在队列中,将它入队:
在这里插入图片描述
继续看第二个出边 1 → 5 5 1\xrightarrow{5}5 15 5,发现dis[5]=∞dis[1]+5=5,将dis[2]更新为5,顶点5不在队列中,将它入队:
在这里插入图片描述
顶点1的出边已经遍历完了,将它出队(将head往后移):
在这里插入图片描述
现在队首的顶点是2,继续重复刚刚的操作,然后出队,继续遍历下一个队首的出边,反反复复,直到队列为空(head==tail)即可,最终队列如下:
在这里插入图片描述

总结一下步骤:

  1. 初始化dis数组,将顶点1加入队列;
  2. 取队首的顶点u,对于u的每一个出边 u → w v u\xrightarrow{w}v uw v,若有dis[v]>dis[u]+w,进入步骤3;否则,进入步骤4;
  3. 更新dis[v]=dis[u]+w,若v不在队列中,将v加入队列。进入步骤4;
  4. 将队首顶点u从队列中移除,若此时队列为空,返回步骤2;否则,算法结束。

贴代码:

#include <iostream>
#include <algorithm>
#include <queue>
#include <stack>
#include <stdio.h>
#include <limits.h>
#include <cmath>
#include <stdlib.h>
#include <string>
#include <vector>
#include <set>
#include <map>
#include <utility>
#include <windows.h>
#include <climits>

#define LL long long
#define INF 999999

using namespace std;

const double pi = atan(1.) * 4.;
const int N = 51;
const int MOD = 1e9 + 7;

int n, m;
int a, b, c;
int dis[N];
bool book[N];//判断顶点是否位于队列
int que[N], head, tail;

struct node {
	int v;
	int w;
	node(int vv, int ww) :v(vv), w(ww) {}
};
//邻接表
vector<node> u[N];

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	
	//准备工作-----------------------------------
	cin >> n >> m;

	for (int i = 1; i <= m; i++) {
		//每条边的起点、终点、权值
		cin >> a >> b >> c;
		u[a].push_back(node(b, c));
	}

	for (int i = 1; i <= n; i++) {
		dis[i] = INF;
	}
	dis[1] = 0;

	//将顶点1加入队列
	que[tail++] = 1;
	book[1] = true;
	
	//核心步骤-----------------------------------
	//结束条件:队列为空
	while (tail != head) {
		int tu = que[head];//取出位于队首的顶点
		//遍历其出边进行松弛操作
		for (int i = 0; i < u[tu].size(); i++) {
			int tv = u[tu][i].v;
			int tw = u[tu][i].w;
			if (dis[tv] > dis[tu] + tw) {
				dis[tv] = dis[tu] + tw;
				// 这个顶点如果不在队列,加入这个队列
				if (book[tv] == false) {
					book[tv] = true;
					que[tail++] = tv;
				}
			}
		}
		head++;//队首顶点出队
	}

	//输出que和dis检验
	cout << "\nque: "; for (int i = 0; i < n; i++) printf("%3d%",que[i]);
	cout << "\ndis: "; for (int i = 1; i <= n; i++) printf("%3d%", dis[i]);
}
/*
input:
5 5
2 3 2
1 2 -3
1 5 5
4 5 2
3 4 3

output:
que:   1  2  5  3  4
dis:   0 -3 -1  2  4
*/
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值