1 应用:计算有边数限制的单源点最短路
Bellman-Ford算法用于在存在负权边的图上,求单源点最短路,时间复杂度 O ( n m ) O(nm) O(nm)。但是因为该算法的改进版SPFA,在求单源点最短路的问题上几乎总是优于Bellman-Ford算法,所以Bellman-Ford算法一般只应用在对边的数量有限制的最短路问题,即限制经过边的数量不超过 k k k。
Bellman-Ford算法属于动态规划算法。
Bellman-Ford算法的基本思想是,如果要求最短路的长度最多为 k k k(如果不限制,那其实就是 k = n − 1 k=n-1 k=n−1,因为 > = n >=n >=n的路径一定有环,也就一定是负环了,但是存在负环的路径不能作为最短路,不然就可以一直转让路径越来越小),那么执行下面的过程:
dist[N]记录距离,last[N]作为dist[N]的副本,两者清空为正无穷
到源点1自己的距离是0,所以dist[1] = 0
循环k次
将上轮算好的dist[N]记录到副本last[N]里,防止更新dist[N]时候用的不是上轮的值
遍历所有m条边{a,b,w},表示从a到b有权值为w的边
更新dist[b] = min(dist[b], last[a] + w),其思路也是看看先到a再到b会不会更短
最后,数组dist[N]
里记录的就是从源点
1
1
1,不超过
k
k
k步,到其它所有点的最短距离。
如果求的是在最长 k k k步这个限制下的最短路,那么时间复杂度是 O ( k m ) O(km) O(km)。
如果求的就是对路径长度 k k k没有要求的最短路,那么 k k k就是 n − 1 n-1 n−1,这个时候算法的时间复杂度是 O ( m n ) O(mn) O(mn)。
由于Bellman-Ford算法只要求能把所有边遍历就行了,所以在实现的时候既不需要用邻接矩阵存,也不需要写邻接表,直接开数组,把所有边存数组里就行了。
struct Edge {
int a, b, w;
}edges[M];
2 应用:检测负权回路
基于前面的思想,如果外层循环做到第
n
n
n轮的时候还有更新(指对dist
的松弛操作在第
n
n
n轮也执行了),那么就一定存在一个长度为
n
n
n的最短路,由于一共就只有
n
n
n个点,所以这条路径上一定有环,也就一定是负环(才会越来越小,使其更新)。
所以Bellman-Ford算法把 k k k设置到 n n n就可以用来检测负权回路,只是一般检测负权回路不用Bellman-Ford算法来做,因为它时间复杂度比较高,而是用它的优化版——SPFA来做。
SPFA算法在很多方面都优于Bellman-Ford算法,所以Bellman-Ford算法主要还是应用在前面说的计算有边数限制的最短路这个问题上。不过如果直接用SPFA算法来求最短路,那么就要求图中不存在负环。
3 模板题:有边数限制的最短路
这题说了可能存在负权回路,但是因为这题是限制了边数不超过 k k k,所以这个时候路径是不能无限长的,也就不能在负环里面无限旋转,所以有负环也一样求最短路,这个“最短路”其实是“限制了边长度的最短路”。
注意,由于有负权边,所以取
m
i
n
min
min更新dist[N]
这个数组的时候,正无穷的last[a]
也是可能拿来更新dist[b]
的,所以最后判断有没有到b
的最短路的时候不能用== inf
来判断,这里是用> inf / 2
就认为是拿inf
来更新过了,所以就不存在到这个点的最短路。
#include <iostream>
#include <cstring>
using namespace std;
const int N = 510, M = 1e4 + 10;
// 边数组,从a到b有权值为w的边
struct Edge {
int a, b, w;
}edges[M];
// n个结点,m条边,最多k步最短路
int n, m, k;
// dist存到每个点不超过i步的最短路,经过k次松弛就是不超过k步的最短路
// last是dist的副本,防止一次外层循环里出现结点之间互相松弛的情况
int dist[N], last[N];
// 求最长不超过k步的最短路,存到dist数组里
void bellman_ford() {
// 清空dist为inf,因为刚循环就会复制到last里,所以last不用清
memset(dist, 0x3f, sizeof dist);
// 源点到源点(编号1)的距离为0
dist[1] = 0;
// 最长不超过k步,所以松弛操作只执行k词
for (int i = 0; i < k; i ++ ) {
// 先将dist暂存到last里
memcpy(last, dist, sizeof dist);
// 对edges里存的每条边做松弛操作
for (int j = 0; j < m; j ++ ) {
auto& e = edges[j];
dist[e.b] = min(dist[e.b], last[e.a] + e.w);
}
}
}
int main() {
cin >> n >> m >> k;
// 所有的边直接读入到数组里
for (int i = 0; i < m; i ++ )
cin >> edges[i].a >> edges[i].b >> edges[i].w;
// 计算经过最多k步的最短路,存到dist里
bellman_ford();
// 注意由于存在负权边,inf可以拿来更新,所以判断不存在是用> inf/2
if (dist[n] > 0x3f3f3f3f / 2) cout << "impossible" << endl;
else cout << dist[n] << endl;
return 0;
}