Files
leetcode-master/problems/kamacoder/0094.城市间货物运输I-SPFA.md
youngyangyang04 7aa973b561 Update
2025-07-06 11:28:34 +08:00

539 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<p align="center"><strong><a href="./qita/join.md">参与本项目</a>,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!</strong></p>
# Bellman_ford 队列优化算法又名SPFA
[卡码网94. 城市间货物运输 I](https://kamacoder.com/problempage.php?pid=1152)
题目描述
某国为促进城市间经济交流,决定对货物运输提供补贴。共有 n 个编号为 1 到 n 的城市,通过道路网络连接,网络中的道路仅允许从某个城市单向通行到另一个城市,不能反向通行。
网络中的道路都有各自的运输成本和政府补贴,道路的权值计算方式为:运输成本 - 政府补贴。
权值为正表示扣除了政府补贴后运输货物仍需支付的费用;权值为负则表示政府的补贴超过了支出的运输成本,实际表现为运输过程中还能赚取一定的收益。
请找出从城市 1 到城市 n 的所有可能路径中,综合政府补贴后的最低运输成本。
如果最低运输成本是一个负数,它表示在遵循最优路径的情况下,运输过程中反而能够实现盈利。
城市 1 到城市 n 之间可能会出现没有路径的情况,同时保证道路网络中不存在任何负权回路。
> 负权回路是指一系列道路的总权值为负,这样的回路使得通过反复经过回路中的道路,理论上可以无限地减少总成本或无限地增加总收益。
输入描述
第一行包含两个正整数,第一个正整数 n 表示该国一共有 n 个城市,第二个整数 m 表示这些城市中共有 m 条道路。
接下来为 m 行每行包括三个整数s、t 和 v表示 s 号城市运输货物到达 t 号城市,道路权值为 v单向图
输出描述
如果能够从城市 1 到连通到城市 n 请输出一个整数,表示运输成本。如果该整数是负数,则表示实现了盈利。如果从城市 1 没有路径可达城市 n请输出 "unconnected"。
输入示例:
```
6 7
5 6 -2
1 2 1
5 3 1
2 5 2
2 4 -3
4 6 4
1 3 5
```
## 背景
本题我们来系统讲解 Bellman_ford 队列优化算法 也叫SPFA算法Shortest Path Faster Algorithm
> SPFA的称呼来自 1994年西南交通大学段凡丁的论文其实Bellman_ford 提出后不久 20世纪50年代末期 就有队列优化的版本,国际上不承认这个算法是是国内提出的。 所以国际上一般称呼 该算法为 Bellman_ford 队列优化算法Queue improved Bellman-Ford
大家知道以上来历,知道 SPFA 和 Bellman_ford 队列优化算法 指的都是一个算法就好。
如果大家还不够了解 Bellman_ford 算法,强烈建议按照《代码随想录》的顺序学习,否则可能看不懂下面的讲解。
大家可以发现 Bellman_ford 算法每次松弛 都是对所有边进行松弛。
但真正有效的松弛,是基于已经计算过的节点在做的松弛。
给大家举一个例子:
![](https://file1.kamacoder.com/i/algo/20240328104119.png)
本图中,对所有边进行松弛,真正有效的松弛,只有松弛 边节点1->节点2 和 边节点1->节点3
而松弛 边节点4->节点6 节点5->节点3等等 都是无效的操作,因为 节点4 和 节点 5 都是没有被计算过的节点。
所以 Bellman_ford 算法 每次都是对所有边进行松弛,其实是多做了一些无用功。
**只需要对 上一次松弛的时候更新过的节点作为出发节点所连接的边 进行松弛就够了**
基于以上思路,如何记录 上次松弛的时候更新过的节点呢?
用队列来记录。(其实用栈也行,对元素顺序没有要求)
## 模拟过程
接下来来举例这个队列是如何工作的。
以示例给出的所有边为例:
```
5 6 -2
1 2 1
5 3 1
2 5 2
2 4 -3
4 6 4
1 3 5
```
我们依然使用**minDist数组来表达 起点到各个节点的最短距离**例如minDist[3] = 5 表示起点到达节点3 的最小距离为5
初始化起点为节点1 起点到起点的最短距离为0所以minDist[1] 为 0。 将节点1 加入队列 下次松弛从节点1开始
![](https://file1.kamacoder.com/i/algo/20240411115555.png)
------------
从队列里取出节点1松弛节点1 作为出发点连接的边节点1 -> 节点2和边节点1 -> 节点3
节点1 -> 节点2权值为1 minDist[2] > minDist[1] + 1 ,更新 minDist[2] = minDist[1] + 1 = 0 + 1 = 1 。
节点1 -> 节点3权值为5 minDist[3] > minDist[1] + 5更新 minDist[3] = minDist[1] + 5 = 0 + 5 = 5。
将节点2、节点3 加入队列,如图:
![](https://file1.kamacoder.com/i/algo/20240411115544.png)
-----------------
从队列里取出节点2松弛节点2 作为出发点连接的边节点2 -> 节点4和边节点2 -> 节点5
节点2 -> 节点4权值为1 minDist[4] > minDist[2] + (-3) ,更新 minDist[4] = minDist[2] + (-3) = 1 + (-3) = -2 。
节点2 -> 节点5权值为2 minDist[5] > minDist[2] + 2 ,更新 minDist[5] = minDist[2] + 2 = 1 + 2 = 3 。
将节点4节点5 加入队列,如图:
![](https://file1.kamacoder.com/i/algo/20240412110348.png)
--------------------
从队列里出去节点3松弛节点3 作为出发点连接的边。
因为没有从节点3作为出发点的边所以这里就从队列里取出节点3就好不用做其他操作如图
![](https://file1.kamacoder.com/i/algo/20240412110420.png)
------------
从队列中取出节点4松弛节点4作为出发点连接的边节点4 -> 节点6
节点4 -> 节点6权值为4 minDist[6] > minDist[4] + 4更新 minDist[6] = minDist[4] + 4 = -2 + 4 = 2 。
将节点6加入队列
如图:
![](https://file1.kamacoder.com/i/algo/20240412110445.png)
---------------
从队列中取出节点5松弛节点5作为出发点连接的边节点5 -> 节点3节点5 -> 节点6
节点5 -> 节点3权值为1 minDist[3] > minDist[5] + 1 ,更新 minDist[3] = minDist[5] + 1 = 3 + 1 = 4
节点5 -> 节点6权值为-2 minDist[6] > minDist[5] + (-2) ,更新 minDist[6] = minDist[5] + (-2) = 3 - 2 = 1
如图将节点3加入队列因为节点6已经在队列里所以不用重复添加
![](https://file1.kamacoder.com/i/algo/20240729161116.png)
所以我们在加入队列的过程可以有一个优化,**用visited数组记录已经在队列里的元素已经在队列的元素不用重复加入**
--------------
从队列中取出节点6松弛节点6 作为出发点连接的边。
节点6作为终点没有可以出发的边。
同理从队列中取出节点3也没有可以出发的边
所以直接从队列中取出,如图:
![](https://file1.kamacoder.com/i/algo/20240411115424.png)
----------
这样我们就完成了基于队列优化的bellman_ford的算法模拟过程。
大家可以发现 基于队列优化的算法要比bellman_ford 算法 减少很多无用的松弛情况,特别是对于边数众多的大图 优化效果明显。
了解了大体流程,我们再看代码应该怎么写。
在上面模拟过程中,我们每次都要知道 一个节点作为出发点连接了哪些节点。
如果想方便知道这些数据,就需要使用邻接表来存储这个图,如果对于邻接表不了解的话,可以看 [kama0047.参会dijkstra堆](./0047.参会dijkstra堆.md) 中 图的存储 部分。
整体代码如下:
```
#include <iostream>
#include <vector>
#include <queue>
#include <list>
#include <climits>
using namespace std;
struct Edge { //邻接表
int to; // 链接的节点
int val; // 边的权重
Edge(int t, int w): to(t), val(w) {} // 构造函数
};
int main() {
int n, m, p1, p2, val;
cin >> n >> m;
vector<list<Edge>> grid(n + 1);
vector<bool> isInQueue(n + 1); // 加入优化,已经在队里里的元素不用重复添加
// 将所有边保存起来
for(int i = 0; i < m; i++){
cin >> p1 >> p2 >> val;
// p1 指向 p2权值为 val
grid[p1].push_back(Edge(p2, val));
}
int start = 1; // 起点
int end = n; // 终点
vector<int> minDist(n + 1 , INT_MAX);
minDist[start] = 0;
queue<int> que;
que.push(start);
while (!que.empty()) {
int node = que.front(); que.pop();
isInQueue[node] = false; // 从队列里取出的时候,要取消标记,我们只保证已经在队列里的元素不用重复加入
for (Edge edge : grid[node]) {
int from = node;
int to = edge.to;
int value = edge.val;
if (minDist[to] > minDist[from] + value) { // 开始松弛
minDist[to] = minDist[from] + value;
if (isInQueue[to] == false) { // 已经在队列里的元素不用重复添加
que.push(to);
isInQueue[to] = true;
}
}
}
}
if (minDist[end] == INT_MAX) cout << "unconnected" << endl; // 不能到达终点
else cout << minDist[end] << endl; // 到达终点最短路径
}
```
## 效率分析
队列优化版Bellman_ford 的时间复杂度 并不稳定,效率高低依赖于图的结构。
例如 如果是一个双向图,且每一个节点和所有其他节点都相连的话,那么该算法的时间复杂度就接近于 Bellman_ford 的 O(N * E) N 为节点数量E为边的数量。
在这种图中,每一个节点都会重复加入队列 n - 1次因为 这种图中 每个节点 都有 n-1 条指向该节点的边,每条边指向该节点,就需要加入一次队列。(如果这里看不懂,可以在重温一下代码逻辑)
至于为什么 双向图且每一个节点和所有其他节点都相连的话,每个节点 都有 n-1 条指向该节点的边, 我再来举个例子,如图:
![](https://file1.kamacoder.com/i/algo/20240416104138.png)
图中 每个节点都与其他所有节点相连节点数n 为 4每个节点都有3条指向该节点的边即入度为3。
n为其他数值的时候也是一样的。
当然这种图是比较极端的情况,也是最稠密的图。
所以如果图越稠密,则 SPFA的效率越接近与 Bellman_ford。
反之图越稀疏SPFA的效率就越高。
一般来说SPFA 的时间复杂度为 O(K * N) K 为不定值,因为 节点需要计入几次队列取决于 图的稠密度。
如果图是一条线形图且单向的话每个节点的入度为1那么只需要加入一次队列这样时间复杂度就是 O(N)。
所以 SPFA 在最坏的情况下是 O(N * E),但 一般情况下 时间复杂度为 O(K * N)。
尽管如此,**以上分析都是 理论上的时间复杂度分析**。
并没有计算 出队列 和 入队列的时间消耗。 因为这个在不同语言上 时间消耗也是不一定的。
以C++为例,以下两段代码理论上,时间复杂度都是 O(n)
```CPP
for (long long i = 0; i < n; i++) {
k++;
}
```
```CPP
for (long long i = 0; i < n; i++) {
que.push(i);
que.front();
que.pop();
}
```
在 MacBook Pro (13-inch, M1, 2020) 机器上分别测试这两段代码的时间消耗情况:
* n = 10^4第一段代码的时间消耗1ms第二段代码的时间消耗 4 ms
* n = 10^5第一段代码的时间消耗1ms第二段代码的时间消耗 13 ms
* n = 10^6第一段代码的时间消耗4ms第二段代码的时间消耗 59 ms
* n = 10^7第一段代码的时间消耗: 24ms第二段代码的时间消耗 463 ms
* n = 10^8第一段代码的时间消耗: 135ms第二段代码的时间消耗 4268 ms
在这里就可以看出 出队列和入队列 其实也是十分耗时的。
SPFA队列优化版Bellman_ford 在理论上 时间复杂度更胜一筹,但实际上,也要看图的稠密程度,如果 图很大且非常稠密的情况下,虽然 SPFA的时间复杂度接近Bellman_ford但实际时间消耗 可能是 SPFA耗时更多。
针对这种情况,我在后面题目讲解中,会特别加入稠密图的测试用例来给大家讲解。
## 拓展
这里可能有录友疑惑,`while (!que.empty())` 队里里 会不会造成死循环? 例如 图中有环,这样一直有元素加入到队列里?
其实有环的情况,要看它是 正权回路 还是 负权回路。
题目描述中,已经说了,本题没有 负权回路 。
如图:
![](https://file1.kamacoder.com/i/algo/20240412111849.png)
正权回路 就是有环,但环的总权值为正数。
在有环且只有正权回路的情况下,即使元素重复加入队列,最后,也会因为 所有边都松弛后节点数值minDist数组不在发生变化了 而终止。
(而且有重复元素加入队列是正常的,多条路径到达同一个节点,节点必要要选择一个最短的路径,而这个节点就会重复加入队列进行判断,选一个最短的)
在[0094.城市间货物运输I](./0094.城市间货物运输I.md) 中我们讲过对所有边 最多松弛 n -1 次,就一定可以求出所有起点到所有节点的最小距离即 minDist数组。
即使再松弛n次以上 所有起点到所有节点的最小距离minDist数组 不会再变了。 (这里如果不理解,建议认真看[0094.城市间货物运输I](./0094.城市间货物运输I.md)讲解)
所以本题我们使用队列优化,有元素重复加入队列,也会因为最后 minDist数组 不会在发生变化而终止。
节点再加入队列,需要有松弛的行为, 而 每个节点已经都计算出来 起点到该节点的最短路径,那么就不会有 执行这个判断条件`if (minDist[to] > minDist[from] + value)`,从而不会有新的节点加入到队列。
但如果本题有 负权回路,那情况就不一样了,我在下一题目讲解中,会重点讲解 负权回路 带来的变化。
## 其他语言版本
### Java
```Java
import java.util.*;
public class Main {
// Define an inner class Edge
static class Edge {
int from;
int to;
int val;
public Edge(int from, int to, int val) {
this.from = from;
this.to = to;
this.val = val;
}
}
public static void main(String[] args) {
// Input processing
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int m = sc.nextInt();
List<List<Edge>> graph = new ArrayList<>();
for (int i = 0; i <= n; i++) {
graph.add(new ArrayList<>());
}
for (int i = 0; i < m; i++) {
int from = sc.nextInt();
int to = sc.nextInt();
int val = sc.nextInt();
graph.get(from).add(new Edge(from, to, val));
}
// Declare the minDist array to record the minimum distance form current node to the original node
int[] minDist = new int[n + 1];
Arrays.fill(minDist, Integer.MAX_VALUE);
minDist[1] = 0;
// Declare a queue to store the updated nodes instead of traversing all nodes each loop for more efficiency
Queue<Integer> queue = new LinkedList<>();
queue.offer(1);
// Declare a boolean array to record if the current node is in the queue to optimise the processing
boolean[] isInQueue = new boolean[n + 1];
while (!queue.isEmpty()) {
int curNode = queue.poll();
isInQueue[curNode] = false; // Represents the current node is not in the queue after being polled
for (Edge edge : graph.get(curNode)) {
if (minDist[edge.to] > minDist[edge.from] + edge.val) { // Start relaxing the edge
minDist[edge.to] = minDist[edge.from] + edge.val;
if (!isInQueue[edge.to]) { // Don't add the node if it's already in the queue
queue.offer(edge.to);
isInQueue[edge.to] = true;
}
}
}
}
// Outcome printing
if (minDist[n] == Integer.MAX_VALUE) {
System.out.println("unconnected");
} else {
System.out.println(minDist[n]);
}
}
}
```
### Python
```Python
import collections
def main():
n, m = map(int, input().strip().split())
edges = [[] for _ in range(n + 1)]
for _ in range(m):
src, dest, weight = map(int, input().strip().split())
edges[src].append([dest, weight])
minDist = [float("inf")] * (n + 1)
minDist[1] = 0
que = collections.deque([1])
visited = [False] * (n + 1)
visited[1] = True
while que:
cur = que.popleft()
visited[cur] = False
for dest, weight in edges[cur]:
if minDist[cur] != float("inf") and minDist[cur] + weight < minDist[dest]:
minDist[dest] = minDist[cur] + weight
if visited[dest] == False:
que.append(dest)
visited[dest] = True
if minDist[-1] == float("inf"):
return "unconnected"
return minDist[-1]
if __name__ == "__main__":
print(main())
```
### Go
### Rust
### JavaScript
```js
async function main() {
// 輸入
const rl = require('readline').createInterface({ input: process.stdin })
const iter = rl[Symbol.asyncIterator]()
const readline = async () => (await iter.next()).value
const [n, m] = (await readline()).split(" ").map(Number)
const grid = {}
for (let i = 0 ; i < m ; i++) {
const [src, desc, w] = (await readline()).split(" ").map(Number)
if (grid.hasOwnProperty(src)) {
grid[src].push([desc, w])
} else {
grid[src] = [[desc, w]]
}
}
const minDist = Array.from({length: n + 1}, () => Number.MAX_VALUE)
// 起始點
minDist[1] = 0
const q = [1]
const visited = Array.from({length: n + 1}, () => false)
while (q.length) {
const src = q.shift()
const neighbors = grid[src]
visited[src] = false
if (neighbors) {
for (const [desc, w] of neighbors) {
if (minDist[src] !== Number.MAX_VALUE
&& minDist[src] + w < minDist[desc]) {
minDist[desc] = minDist[src] + w
if (!visited[desc]) {
q.push(desc)
visited[desc] = true
}
}
}
}
}
// 輸出
if (minDist[n] === Number.MAX_VALUE) {
console.log('unconnected')
} else {
console.log(minDist[n])
}
}
main()
```
### TypeScript
### PhP
### Swift
### Scala
### C#
### Dart
### C