【专题】Bellman-Ford

简介

Bellman-Ford算法是求解单源最短路的算法之一,适用于可包含负边权的有向和无向图,可以判断是否包含负环(注意,如果是包含负权回路则不存在最短路问题)。时间复杂度\(O(nm)\)

Bellman-Ford算法

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

核心代码如下:

1
2
3
4
5
6
7
8
9
10
int bellman(int s, int n, int m)
{
dis[s] = 0;
for(int k=1; k<n; k++){
for(int i=1; i<=m; i++){
dis[v[i]] = min(dis[v[i]], dis[u[i]]+w[i]);
}
}
return 0;
}

如果在n-1次松弛之后,还有可以进行松弛的边,那么就存在负环(负权回路):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int bellman(int s, int n, int m)
{
dis[s] = 0;
for(int k=1; k<n; k++){
for(int i=1; i<=m; i++){
if(dis[u[i]]+w[i] < dis[v[i]]){
dis[v[i]] = dis[u[i]]+w[i];
pre[v[i]] = u[i];
}
}
}
for(int i=1; i<=m; i++){
if(dis[u[i]]+w[i] < dis[v[i]]){
return -1;
}
}
return 0;
}
收起代码

SPFA

SPFA是Bellman-Ford算法的队列优化,核心思想,只有那些在前一遍松弛中改变了距离估计值的点,才可能引起它们的邻接点的距离估计值的改变。为什么队列为空就不改变了呢?就是因为要到下一点必须经过它的前一个邻接点。

设立一个先进先出的队列q用来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作,如果v点的最短路径估计值有所调整,且v点不在当前的队列中,就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止。

核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int spfa(int s, int n)
{
dis[s] = 0; vis[s] = 1; q.push(s);
while(!q.empty())
{
int u = q.front(); q.pop(); vis[u] = 0;
for(int i=H[u]; ~i; i=e[i].next){
int v = e[i].to, w = e[i].w;
if(dis[u]+w < dis[v]){
dis[v] = dis[u]+w;
if(!vis[v]) q.push(v), vis[v] = 1;
}
}
}
return 0;
}
收起代码

其它优化

除了队列优化(SPFA)之外,Bellman-Ford 还有其他形式的优化,这些优化在部分图上效果明显,但在某些特殊图上,最坏复杂度可能达到指数级。

  • 堆优化:将队列换成堆,与 Dijkstra 的区别是允许一个点多次入队。在有负权边的图可能被卡成指数级复杂度。
  • 栈优化:将队列换成栈(即将原来的 BFS 过程变成 DFS),在寻找负环时可能具有更高效率,但最坏时间复杂度仍然为指数级。
  • LLL 优化:将普通队列换成双端队列,每次将入队结点距离和队内距离平均值比较,如果更大则插入至队尾,否则插入队首。
  • SLF 优化:将普通队列换成双端队列,每次将入队结点距离和队首比较,如果更大则插入至队尾,否则插入队首。

更多优化以及针对这些优化的 Hack 方法,可以看 fstqwq 在知乎上的回答

模板

Bellman-Ford算法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
const int inf = 0x3f3f3f3f;
const int mxn = 1e3 + 5;

int u[mxn], v[mxn], w[mxn];
int dis[mxn], pre[mxn];

void bellman_init(int n)
{
for(int i=1; i<=n; i++){
dis[i] = inf;
pre[i] = 0;
}
}

int bellman(int s, int n, int m)
{
int k=0, f=1; dis[s] = 0;
while(f)
{
if(++k > n) return -1;
f = 0;
for(int i=1; i<=m; i++){
if(dis[u[i]]+w[i] < dis[v[i]]){
dis[v[i]] = dis[u[i]]+w[i];
pre[v[i]] = u[i];
f = 1;
}
}
}
return 0;
}

int main()
{
int n, m;
scanf("%d %d", &n, &m);

for(int i=1; i<=m; i++)
{
scanf("%d %d %d", &u[i], &v[i], &w[i]);
}

bellman_init(n);
if(bellman(1, n, m) == -1)
{
printf("包含负权回路\n");
}
else
{
for(int i=1; i<=n; i++)
printf("%d ", dis[i]);
printf("\n");

for(int i=1; i<=n; i++)
printf("%d ", pre[i]);
printf("\n");
}

return 0;
}

/*
Sample Input:

4 5
1 2 2
1 3 4
2 4 1
3 2 1
3 4 8


Sample Output:

0 2 4 3
0 1 1 2

*/
收起代码

SPFA:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
const int inf = 0x3f3f3f3f;
const int mxn = 1e3 + 5;

struct E {
int to, next, w;
} e[mxn];

int H[mxn], tot;

void add(int from, int to, int w) {
e[tot] = {to, H[from], w};
H[from] = tot++;
}

void graph_init(int n)
{
for(int i=1; i<=n; i++)
H[i] = -1;
tot = 0;
}

int dis[mxn], pre[mxn], num[mxn];
bool vis[mxn];
queue<int> q;

void spfa_init(int n)
{
for(int i=1; i<=n; i++){
dis[i] = inf;
num[i] = pre[i] = 0;
}
}

int spfa(int s, int n)
{
dis[s] = 0; q.push(s);
num[s] = vis[s] = 1;
while(!q.empty())
{
int u = q.front(); q.pop(); vis[u] = 0;
for(int i=H[u]; ~i; i=e[i].next){
int v = e[i].to, w = e[i].w;
if(dis[u]+w < dis[v]){
dis[v] = dis[u]+w;
pre[v] = u;
if(!vis[v]){
q.push(v), vis[v] = 1;
if(++num[v] > n) return -1;
}
}
}
}
return 0;
}

int main()
{
int n, m;
scanf("%d %d", &n, &m);
graph_init(n);

for(int i=1; i<=m; i++)
{
int u, v, w;
scanf("%d %d %d", &u, &v, &w);
add(u, v, w);
}

spfa_init(n);
if(spfa(1, n) == -1)
{
printf("包含负权回路\n");
}
else
{
for(int i=1; i<=n; i++)
printf("%d ", dis[i]);
printf("\n");

for(int i=1; i<=n; i++)
printf("%d ", pre[i]);
printf("\n");
}

return 0;
}


/*
Sample Input:

4 5
1 2 2
1 3 4
2 4 1
3 2 1
3 4 8


Sample Output:

0 2 4 3
0 1 1 2

*/
收起代码