Bellman-Ford算法实现带有负权边的单源最短路

发布时间 2024-01-01 12:12:08作者: 凪风sama

Bellman-Ford算法

对于Dijkstra算法,不妨给出这样一个例子

graph LR A((A)) -->|1| C((C)) A -->|2|D((D)) D -->|-4| C

根据Dijkstra算法的流程,选取A为源点。更新与A邻接的顶点,有C和D。选取已更新顶点中距离A的最小值,显然选择边权为1的边所连接的顶点C,并将C收入最短路集合S中,此时已经确定A->C的最短路为1。那么问题就出现了。
由于已经收入S中的顶点都视为最短路上的顶点且不可更改,因此由Dijkstra算法确定的A->C的最短路就是1。但是显然实际的最短路是A->D->C,花销为-2。

这时我们就需要使用别的方法来求出其最短路。比如Bellman-Ford以及其使用队列优化后的SPFA

Bellman-Ford的实现

Bellman-Ford算法实际上采用动态规划的思想。首先大前提是一个结论,对于有n个顶点的图,从源点到达其他任意顶点最多经过n-1条边。
定义\(dp[i][j]\),代表最多经过\(i\)条边到达顶点\(j\)的最小花销
显然\(dp[0][j] = +\infty\)
对于给定\(dp[i][j]\),考虑其最小花销,两个方面。

  1. 考虑从前一个顶点 k 到达顶点 j ,\(dp[i][j] = dp[i-1][k] + w[k->j]\)
  2. 考虑不经过新的中间顶点(即维持原状) ,\(dp[i][j] = dp[i - 1][k]\)

状态转移方程为\(dp[i][j] = min(dp[i - 1][j] , dp[i - 1][k] + w[k ->j])\)
显然在状态转移时,更新本层的状态只用到了上一层的状态,不妨加一个滚动数组优化,使用一维数组即可。

最终的代码如下
配合例题853. 有边数限制的最短路 - AcWing题库

#include <iostream>
#include <cstring>
using namespace std;
const int N = 1e5 + 20;
int n, m, k;
struct Edge
{
	int v;
	int u;
	int w;
}edges[N];
int dist[N],last[N];
void bellman_ford()
{
	memset(dist, 0x3f, sizeof dist);
	dist[1] = 0;
	for (int i = 0; i < k; i++)//这里k的含义就是经过不超过k条边的最短路
	{
		memcpy(last, dist, N); //last保存上一层的状态。
		for (auto x : edges)
		{
			dist[x.v] = min(dist[x.v], last[x.u] + x.w);//更新使用上一层的状态来更新
		}
	}
}
int main()
{
	cin >> n >> m >> k;
	for (int i = 0; i < m; i++)
	{
		int v, u, w;
		cin >> v >> u >> w;
		edges[i] = { v,u,w };
	}
	bellman_ford();
	if (dist[n] > 0x3f3f3f3f / 2)
		puts("impossible");
	else
		cout << dist[n];
	return 0;
}

这里使用last数组记录上一层的状态。可以看出,当要求求出最多经过k条边的最短路时,只可以使用bellman-ford来做。同时由于动态规划更新状态不需要有序,因此可以简化图,只存储边

使用Bellman-Ford判断负值圈

所谓负值圈就是一个权值和为负数的环,如下

flowchart LR A((A)) -->|2| B((B)) B -->|-5| C((C)) C -->|1| A

-5 + 2 + 1 = -2,这就形成了一个负环。
存在负值圈的图一定无最短路
但是使用Bellman-Ford算法可以检测出图中是否有负值圈。
使用一个数组cnt[N]维护更新到第N个节点所经过的边数。由于从源点到达其他任意顶点最多经过n-1条边,但是对于有负值圈的图,由于总是会有更小的路径,因此其经过的边数会大于n。我们仅需要检测在某次更新中cnt[i]是否会大于n-1即可