Bellman-Ford算法(解决负权边)

核心代码:

for (int k = 1; k <= n - 1; ++k){
    
    
	for (int i = 1; i <= m; ++i){
    
    
		if(dis[v[i]] > dis[u[i]] + w[i])
			dis[v[i]] = dis[u[i]] + w[i];
	}
} 

上面的代码外层循环一共循环了n - 1次(n为顶点个数), 内层循环循环了m次(m为每一条边),即枚举每一条边。dis数组的作用与Dijkstra算法一样,是用来记录源点到其余各个顶点的最短路径的。u、v、w三个数组是用来记录边的信息的。例如:第i条边存储在u[i]、v[i]、和w[i]中, 表示从顶点u[i]到顶点v[i]这条边(u[i]->v[i])权值为w[i]。

	if(dis[v[i]] > dis[u[i]] + w[i])
		dis[v[i]] = dis[u[i]] + w[i];

上面这行代码的意思是:看看能否通过u[i]->v[i](权值为w[i])这条边,使得1号顶点到v[i]号顶点的距离(dis[u[i]])加上u[i]->v[i](权值为w[i])这条边的值是否会比原先1号顶点到v[i]号顶点的距离(dis[v[i]])要小。这一点其实是与Dijkstra算法中的“松弛”操作是一样的。现在我们要把所有的边松弛一遍,代码如下:

for (int i = 1; i <= m; ++i){
    
    
	if(dis[v[i]] > dis[u[i]] + w[i])
		dis[v[i]] = dis[u[i]] + w[i];
}

把每条边都松弛一遍之后会有什么效果呢?现在来举个例子。求下图1号顶点到其余各点的最短路径。
在这里插入图片描述
我们还是用一个dis数组来存储1号顶点到所有顶点的距离。
在这里插入图片描述
上方右图中每个顶点旁的值(带下划线的数字)为该顶点的最短路“估计值”(当前1号顶点到该顶点的距离),即数组dis中对应的值。根据边给出的顺序,先来处理第一条边“2 3 2”(2->3,权值为2,通过这条边进行松弛)。即判断dis[3]是否大于dis[2] + 2。此时dis[3]是inf,dis[2]是inf,因此dis[2] + 2也是inf,所以通过“2 3 2”这条边不能使dis[3]的值变小,松弛失败。
同理,继续处理第2条边“1 2 -3”, 我们发现dis[2]大于dis[1] + (-3),通过这条边可以使dis[2]的值从inf变为-3,因此松弛成功。用同样的方法处理剩下的每一条边。对所有的边松弛一遍后的结果如下。
在这里插入图片描述
我们发现,在对每条边都进行一次松弛后,已经使得dis[2]和dis[5]的值变小,即1号顶点到2号顶点和1号顶点到5号顶点的距离都变短了。
接下来我们需要对所有的边再进行一轮松弛,看看又会发生什么变化。
在这里插入图片描述
在这一轮松弛时,我们发现,现在通过“2 3 2”这条边,可以使1号顶点到3号顶点的距离(dis[3])变短了。这一条边在上一轮也松弛过,但上一轮松弛失败了,这一轮却成功了。这是因为在第一轮松弛过后,1号顶点到2号顶点的距离(dis[2])已经发生了变化,这一轮再通过“2 3 2”这条边松弛的时候已经可以使1号顶点到3号顶点的距离(dis[3]的值)变小。
换句话说,第1轮在对所有的边进行松弛之后,得到的是从1号顶点“只能经过一条边”到达其余各个顶点的最短路径长度。第2轮在对所有的边进行松弛之后,得到的是从1号顶点“最多经过两条边”到达其余各顶点的最短路径长度。如果进行k轮的话,得到的就是1号顶点“最多经过k条边”到达其余各个顶点的最短路径长度。现在问题又来了:需要进行多少轮呢?
**只需要进行n - 1轮就可以了。**因为在一个含有n个顶点的图中,任意两点之间的最短路径最多包括n - 1条边。
真的只能包含n - 1条边吗?最短路径中不能包含回路吗?
答案是:不可能!回路分为正权回路(即回路权值之和为正)和负权回路(回路权值之和为负)。如果最短路径中包含正权回路,那么去掉这个回路,一定可以得到更短的路径。如果最短路径中包含负权回路,那么肯定没有最短路径,因为每多走一次负权回路就可以得到更短的路径。因此,最短路径肯定是一个不包含回路的简单路径,即最多包含n - 1条边,所以进行n - 1轮松弛就可以了。
回到之前的例子。这里只需要进行4轮松弛操作就可以了,因为这个图一共有5个顶点。


这里貌似不用进行第4轮嘛,因为进行第4轮之后dis数组没有发生任何变化!没错!其实是最多进行n - 1轮松弛。
整个Bellman-Ford算法用一句话概括就是:对所有的边进行n - 1次“松弛”操作。

完整代码

OK,总结一下。因为最短路径上最多有n - 1条边,因此Bellman-Ford算法最多有n - 1个阶段。在每一个阶段,我们对每一条边都要执行松弛操作。其实每实施一次松弛操作,就会有一些顶点已经求得其最短路,即这些顶点的最短路的“估计值”变为“确定值”。此后这些顶点的最短路的值就会一直保持不变,不再受后续松弛操作的影响(但是,每次还是会判断是否需要松弛,这里浪费了时间,是否可以优化呢?)。在前k个阶段结束后,就已经找出了从源点出发“最多经过k条边”到达各个顶点的最短路。直到进行完n - 1个阶段后,便得出了最多经过n - 1条边的最短路。
Bellman-Ford算法完整代码如下:

//完整代码
#include <bits/stdc++.h>
using namespace std;
#define inf 0x3f3f3f3f
int main(){
    
    
	int dis[10], n, m, u[10], v[10], w[10];
	//读入n和m,n表示顶点个数,m表示边的条数。
	cin >> n >> m;
	//读入边
	for (int i = 1; i <= m; ++i){
    
    
		cin >> u[i] >> v[i] >> w[i];
	} 
	//初始化dis数组,这里是1号顶点到其余各个顶点的初始路程。
	for (int i = 1; i <= n; ++i){
    
    
		dis[i] = inf;
	} 
	dis[1] = 0;
	//Bellman-Ford算法核心语句 
	for (int k = 1; k <= n - 1; ++k){
    
    
		for (int i = 1; i <= m; ++i){
    
    
			if(dis[v[i]] > dis[u[i]] + w[i])
				dis[v[i]] = dis[u[i]] + w[i];
		}
	} 
	//输出最终的结果 
	for (int i = 1; i <= n; ++i){
    
    
		cout << dis[i] << ' ';
	} 
	return 0;
} 

//5 5
//2 3 2
//1 2 -3
//1 5 5
//4 5 2
//3 4 3

//运行结果
//0 -3 -1 2 4 

Bellman-Ford算法检测有无负权回路

如果在进行n - 1轮松弛之后,仍然存在:

if(dis[v[i]] > dis[u[i]] + w[i])
	dis[v[i]] = dis[u[i]] + w[i];

的情况,也就是说在进行n - 1轮松弛之后我们依然可以继续成功松弛,那么此图必然存在负权回路, 关键代码如下:

// Bellman-Ford算法检测图中有无负权回路
// Bellman-Ford算法核心语句 
for (int k = 1; k <= n - 1; ++k){
    
    
	for (int i = 1; i <= m; ++i){
    
    
		if(dis[v[i]] > dis[u[i]] + w[i])
			dis[v[i]] = dis[u[i]] + w[i];
	}
} 
//检测负权回路 
for (int i = 1; i <= m; ++i)
	if(dis[v[i]] > dis[u[i]] + w[i]) flag = 1;
if(flag) cout << "此图含有负权回路"; 

Bellman-Ford算法的优化

显然, Bellman-Ford算法的时间复杂度是O(NM),这个复杂度比Dijkstra算法还高,我们可以对其进行优化。在实际操作中,Bellman-Ford算法经常会在未达到n - 1轮松弛前就已经计算出最短路,之前我们已经说过n - 1其实是最大值。因此可以添加一个变量check来标记数组dis在本轮松弛中是否发生了变化,如果没有发生变化,则提前跳出循环,代码如下:

#include <bits/stdc++.h>
using namespace std;
#define inf 0x3f3f3f3f
int main(){
    
    
	int dis[10], bak[10], n, m, u[10], v[10], w[10], check, flag;
	//读入n和m,n表示顶点个数,m表示边的条数
	cin >> n >> m; 
	//读入边
	for (int i = 1; i <= m; ++i){
    
    
		cin >> u[i] >> v[i] >> w[i];
	} 
	//初始化dis数组,这里是1号顶点到其余各个顶点的初始路程
	for (int i = 1;i <= n; ++i){
    
    
		dis[i] = inf;
	} 
	dis[1] = 0;
	//Bellman-Ford算法核心语句 
	for (int k = 1; k <= n - 1; ++k){
    
    
		check = 0;//用来标记在本轮松弛中dis数组是否会发生更新
		//进行一轮松弛 
		for (int i = 1; i <= m; ++i){
    
    
			if(dis[v[i]] > dis[u[i]] + w[i]){
    
    
				dis[v[i]] = dis[u[i]] + w[i];
				check = 1; 
			}
		}
		//松弛完毕后检测数组dis是否有更新
		if(check == 0) break;//如果数组dis没有更新,提前退出循环结束算法 
	} 
	//检测负权回路 
	flag = 0;
	for (int i = 1; i <= m; ++i){
    
    
		if (dis[v[i]] > dis[u[i]] + w[i]) flag = 1;
	} 
	if(flag) cout << "此图含有负权回路";
	else{
    
    
		//输出最终的结果
		for (int i = 1; i <= m; ++i){
    
    
			cout << dis[i] << ' ';
		} 
	} 
	return 0;
}

Bellman-Ford算法的另一种优化:在每实施一次松弛操作后,就会有一些顶点已经求得其最短路,此后这些顶点的估计值就会一直保持不变,不再后续松弛操作的影响,但是每次还要判断是否需要松弛,这里浪费了时间。这就启发我们:每次仅对最短路估计值发生变化了的顶点的所有边执行松弛操作。

猜你喜欢

转载自blog.csdn.net/LXC_007/article/details/113737560
今日推荐