提示:DDU,供自己复习使用。欢迎大家前来讨论~
文章目录
图论part10
Bellman_ford 队列优化算法(又名SPFA)
题目:94. 城市间货物运输
SPFA算法的重点:
-
队列优化:SPFA算法是对Bellman-Ford算法的优化,它使用队列来存储已经更新过距离的节点,而不是对所有节点进行松弛操作。这样可以减少不必要的迭代,因为只有那些距离被更新的节点才可能影响其他节点的最短路径。
-
基于实际更新:在SPFA算法中,只有当某个节点的距离估计值被更新时,该节点才会被加入队列。这意味着算法只关注那些可能影响最终最短路径计算的节点,从而提高了算法的效率。
SPFA算法的经典例子:
假设我们有一个城市之间的交通网络,城市用节点表示,道路用有向边表示,道路的权重(费用)表示为边的标签。我们要求从城市A到城市F的最短路径。以下是这个网络的示意图:
A --(2)--> B --(3)--> C --(4)--> D
| | | |
+----(-3)---+ +----(-2)---+
| | |
+----------------(5)--+ |
| |
+----(3)---+
| |
E --(1)--> F
在这个图中,边的权重有正有负,例如,A到B是2,B到E是-3。
初始步骤:
- 设置源点A的距离为0(
dist[A] = 0
),其他所有点的距离为无穷大。
SPFA算法过程:
- 将源点A加入队列。
- 当队列不为空时,重复以下步骤:
- 取出队列头部的节点U。
- 遍历节点U的所有邻接节点V,如果通过U到V的路径更短(即
dist[U] + weight[U-V] < dist[V]
),则更新dist[V]
,并将V加入队列。
模拟过程:
- 初始时,队列中只有A,
dist[A] = 0
,其他dist[X] = ∞
。 - 第一次循环,取出A,更新B(
dist[B] = 2
),将B入队。 - 接下来,取出B,更新E(
dist[E] = 2 - 3 = -1
),将E入队。 - 然后,取出E,更新F(
dist[F] = -1 + 1 = 0
),将F入队。 - 继续这个过程,直到队列为空。
最终结果:
- 我们发现从A到F的最短路径是经过B和E,总费用为0。
这个例子展示了SPFA算法如何处理带有负权边的图,并找到最短路径。在这个过程中,算法有效地利用了队列来减少不必要的迭代,提高了计算效率。
整体代码如下:
#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; // 到达终点最短路径
}
用于求解有向图中从起点到终点的最短路径问题,特别是当图中存在负权边时。
-
数据结构:
Edge
结构体用于表示图的边,包含目标节点to
和边的权重val
。vector<list<Edge>> grid
用于存储图的邻接表,表示节点间的连接关系和权重。vector<int> minDist
用于存储从起点到每个节点的最短距离,初始化为无穷大INT_MAX
。queue<int> que
用于实现SPFA算法中的队列,存储待处理的节点。vector<bool> isInQueue
用于标记节点是否已经在队列中,以避免重复处理。
-
算法流程:
- 读取节点数
n
、边数m
,以及所有的边p1 -> p2
和对应的权重val
,构建图的邻接表。 - 设置起点
start
到自己的最短距离为0,并将其加入队列。 - 当队列不为空时,执行循环:
- 从队列中取出一个节点
node
。 - 遍历
node
的所有邻接边,如果通过当前节点到邻接节点to
的路径更短,则更新最短距离minDist[to]
。 - 如果邻接节点
to
不在队列中,将其加入队列,并标记为已在队列中。
- 从队列中取出一个节点
- 循环结束后,检查终点
end
的最短距离,如果仍然是INT_MAX
,则表示起点到终点不连通,输出"unconnected";否则输出最短距离。
- 读取节点数
-
关键点:
- SPFA算法通过队列来优化Bellman-Ford算法,减少不必要的边的松弛操作。
- 使用
isInQueue
数组来避免节点被重复加入队列,提高算法效率。 - 算法能够处理负权边,并且能够检测出负权环(如果算法结束后仍有节点可以继续松弛,则存在负权环)。
bellman_ford之判断负权回路
题目:95. 城市间货物运输 II
95. 城市间货物运输 II (kamacoder.com)
使用Bellman-Ford算法判断负权回路的思路:
-
执行Bellman-Ford算法:对图中的所有边进行n-1次松弛操作,其中n是图中节点的数量。这将确保如果没有负权回路,最短路径已经被正确计算。
-
检查进一步的松弛:在完成n-1次松弛后,再次执行一次松弛操作。如果在这次额外的松弛中,任何节点的距离值发生了变化,这表明图中存在负权回路。
-
结论:如果最后一次松弛后没有节点的距离发生变化,那么可以确定图中没有负权回路。如果有任何变化,那么图中存在负权回路,这意味着最短路径无法确定,因为可以通过不断绕负权回路来无限减小路径的总权重。
以上为理论分析,接下来我们再画图举例。
拿题目中示例来画一个图:
图中 节点1 到 节点4 的最短路径是多少(题目中的最低运输成本) (注意边可以为负数的)
节点1 -> 节点2 -> 节点3 -> 节点4,这样的路径总成本为 -1 + 1 + 1 = 1
而图中有负权回路:
那么我们在负权回路中多绕一圈,我们的最短路径 是不是就更小了 (也就是更低的运输成本)
节点1 -> 节点2 -> 节点3 -> 节点1 -> 节点2 -> 节点3 -> 节点4,这样的路径总成本 (-1) + 1 + (-1) + (-1) + 1 + (-1) + 1 = -1
如果在负权回路多绕两圈,三圈,无穷圈,那么我们的总成本就会无限小, 如果要求最小成本的话,你会发现本题就无解了。
在 bellman_ford 算法中,松弛 n-1 次所有的边 就可以求得 起点到任何节点的最短路径,松弛 n 次以上,minDist数组(记录起到到其他节点的最短距离)中的结果也不会有改变 而本题有负权回路的情况下,一直都会有更短的最短路,所以 松弛 第n次,minDist数组 也会发生改变。
那么解决本题的 核心思路,就是在 kama94.城市间货物运输I 的基础上,再多松弛一次,看minDist数组 是否发生变化。
完整代码如下:
#include <iostream>
#include <vector>
#include <list>
#include <climits>
using namespace std;
int main() {
int n, m, p1, p2, val;
cin >> n >> m;
vector<vector<int>> grid;
for(int i = 0; i < m; i++){
cin >> p1 >> p2 >> val;
// p1 指向 p2,权值为 val
grid.push_back({p1, p2, val});
}
int start = 1; // 起点
int end = n; // 终点
vector<int> minDist(n + 1 , INT_MAX);
minDist[start] = 0;
bool flag = false;
for (int i = 1; i <= n; i++) { // 这里我们松弛n次,最后一次判断负权回路
for (vector<int> &side : grid) {
int from = side[0];
int to = side[1];
int price = side[2];
if (i < n) {
if (minDist[from] != INT_MAX && minDist[to] > minDist[from] + price) minDist[to] = minDist[from] + price;
} else { // 多加一次松弛判断负权回路
if (minDist[from] != INT_MAX && minDist[to] > minDist[from] + price) flag = true;
}
}
}
if (flag) cout << "circle" << endl;
else if (minDist[end] == INT_MAX) {
cout << "unconnected" << endl;
} else {
cout << minDist[end] << endl;
}
}
- 时间复杂度: O(N * E) , N为节点数量,E为图中边的数量
- 空间复杂度: O(N) ,即 minDist 数组所开辟的空间
bellman_ford之单源有限最短路
题目:96. 城市间货物运输 III
96. 城市间货物运输 III (kamacoder.com)
对之前讨论的Bellman-Ford算法的一个扩展。
相同点:
- 算法基础:都使用Bellman-Ford算法的基础思想,即通过不断松弛边来迭代更新最短路径的估计值。
- 松弛操作:两种问题都涉及到对图中所有边进行松弛操作,以更新节点的最短路径估计。
不同点:
- 问题限制:原始Bellman-Ford算法目的是找到从单一源点到所有其他节点的最短路径,而这个问题添加了一个额外的限制,即路径最多只能经过k个城市。
- 迭代次数:
- 原始Bellman-Ford算法需要迭代n-1次,其中n是节点数,以确保找到从源点到所有节点的最短路径。
- 在这个变体问题中,需要迭代k+1次,其中k是允许的最大城市数量。这是因为如果最多可以经过k个城市,那么最多会有k+1条边与源点相连(包括源点到第一个城市、第一个城市到第二个城市,依此类推,直到最后一个城市到终点)。
关键理解:
- 在这个问题中,我们不是寻找到每个节点的最短路径,而是在给定的城市数量限制下,寻找到终点的最短路径。
- 通过限制松弛次数为k+1,我们实际上是在限制路径的长度,即路径的边数不超过k+1。
算法的应用:
- 这种变体问题在实际中很有用,比如在旅行规划中,可能希望在访问一定数量的城市后到达目的地,同时希望旅行的总距离尽可能短。
这个变体问题在Bellman-Ford算法的基础上增加了一个路径长度的限制条件,使得算法的应用更加灵活和多样化。
可以写出如下代码:
// 版本二
#include <iostream>
#include <vector>
#include <list>
#include <climits>
using namespace std;
int main() {
int src, dst,k ,p1, p2, val ,m , n;
cin >> n >> m;
vector<vector<int>> grid;
for(int i = 0; i < m; i++){
cin >> p1 >> p2 >> val;
grid.push_back({p1, p2, val});
}
cin >> src >> dst >> k;
vector<int> minDist(n + 1 , INT_MAX);
minDist[src] = 0;
vector<int> minDist_copy(n + 1); // 用来记录上一次遍历的结果
for (int i = 1; i <= k + 1; i++) {
minDist_copy = minDist; // 获取上一次计算的结果
for (vector<int> &side : grid) {
int from = side[0];
int to = side[1];
int price = side[2];
// 注意使用 minDist_copy 来计算 minDist
if (minDist_copy[from] != INT_MAX && minDist[to] > minDist_copy[from] + price) {
minDist[to] = minDist_copy[from] + price;
}
}
}
if (minDist[dst] == INT_MAX) cout << "unreachable" << endl; // 不能到达终点
else cout << minDist[dst] << endl; // 到达终点最短路径
}
- 时间复杂度: O(K * E) , K为至多经过K个节点,E为图中边的数量
- 空间复杂度: O(N) ,即 minDist 数组所开辟的空间
Bellman-Ford算法的三个关键点:
-
负权边处理:Bellman-Ford算法能够处理图中包含负权边的情况,这与Dijkstra算法不同,后者只适用于非负权边的图。(负权边没问题:即使道路有的收费比别的低(负权边),这个算法也能算。)
-
动态规划求解:算法通过动态规划的方法,对所有边进行n-1次松弛操作,其中n是图中节点的数量,以此来迭代更新每个节点的最短路径估计。(多轮更新:一遍遍地检查和更新路径,直到找不出更短的路。
) -
负权环检测:在进行n次松弛操作后,如果发现任何节点的最短路径估计值发生了变化,这表明图中存在负权环,因为这意味着可以通过不断经过这个环来无限减少路径的总权重。(揪出无限绕圈,能发现那种可以一直绕、一直省钱的环路。)