代码随想录Day 60|SPFA算法,bellman_ford之判断负权回路,bellman_ford之单源有限最短路,题目:城市间货物运输

提示:DDU,供自己复习使用。欢迎大家前来讨论~

图论part10

Bellman_ford 队列优化算法(又名SPFA)

题目:94. 城市间货物运输

94. 城市间货物运输 I (kamacoder.com)

SPFA算法的重点

  1. 队列优化:SPFA算法是对Bellman-Ford算法的优化,它使用队列来存储已经更新过距离的节点,而不是对所有节点进行松弛操作。这样可以减少不必要的迭代,因为只有那些距离被更新的节点才可能影响其他节点的最短路径。

  2. 基于实际更新:在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算法过程

  1. 将源点A加入队列。
  2. 当队列不为空时,重复以下步骤:
    • 取出队列头部的节点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; // 到达终点最短路径
}

用于求解有向图中从起点到终点的最短路径问题,特别是当图中存在负权边时。

  1. 数据结构

    • Edge结构体用于表示图的边,包含目标节点to和边的权重val
    • vector<list<Edge>> grid用于存储图的邻接表,表示节点间的连接关系和权重。
    • vector<int> minDist用于存储从起点到每个节点的最短距离,初始化为无穷大INT_MAX
    • queue<int> que用于实现SPFA算法中的队列,存储待处理的节点。
    • vector<bool> isInQueue用于标记节点是否已经在队列中,以避免重复处理。
  2. 算法流程

    • 读取节点数n、边数m,以及所有的边p1 -> p2和对应的权重val,构建图的邻接表。
    • 设置起点start到自己的最短距离为0,并将其加入队列。
    • 当队列不为空时,执行循环:
      • 从队列中取出一个节点node
      • 遍历node的所有邻接边,如果通过当前节点到邻接节点to的路径更短,则更新最短距离minDist[to]
      • 如果邻接节点to不在队列中,将其加入队列,并标记为已在队列中。
    • 循环结束后,检查终点end的最短距离,如果仍然是INT_MAX,则表示起点到终点不连通,输出"unconnected";否则输出最短距离。
  3. 关键点

    • SPFA算法通过队列来优化Bellman-Ford算法,减少不必要的边的松弛操作。
    • 使用isInQueue数组来避免节点被重复加入队列,提高算法效率。
    • 算法能够处理负权边,并且能够检测出负权环(如果算法结束后仍有节点可以继续松弛,则存在负权环)。

bellman_ford之判断负权回路

题目:95. 城市间货物运输 II

95. 城市间货物运输 II (kamacoder.com)

使用Bellman-Ford算法判断负权回路的思路

  1. 执行Bellman-Ford算法:对图中的所有边进行n-1次松弛操作,其中n是图中节点的数量。这将确保如果没有负权回路,最短路径已经被正确计算。

  2. 检查进一步的松弛:在完成n-1次松弛后,再次执行一次松弛操作。如果在这次额外的松弛中,任何节点的距离值发生了变化,这表明图中存在负权回路。

  3. 结论:如果最后一次松弛后没有节点的距离发生变化,那么可以确定图中没有负权回路。如果有任何变化,那么图中存在负权回路,这意味着最短路径无法确定,因为可以通过不断绕负权回路来无限减小路径的总权重。

以上为理论分析,接下来我们再画图举例。

拿题目中示例来画一个图:

img

图中 节点1 到 节点4 的最短路径是多少(题目中的最低运输成本) (注意边可以为负数的)

节点1 -> 节点2 -> 节点3 -> 节点4,这样的路径总成本为 -1 + 1 + 1 = 1

而图中有负权回路:

img

那么我们在负权回路中多绕一圈,我们的最短路径 是不是就更小了 (也就是更低的运输成本)

节点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算法的一个扩展。

相同点

  1. 算法基础:都使用Bellman-Ford算法的基础思想,即通过不断松弛边来迭代更新最短路径的估计值。
  2. 松弛操作:两种问题都涉及到对图中所有边进行松弛操作,以更新节点的最短路径估计。

不同点

  1. 问题限制:原始Bellman-Ford算法目的是找到从单一源点到所有其他节点的最短路径,而这个问题添加了一个额外的限制,即路径最多只能经过k个城市。
  2. 迭代次数
    • 原始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算法的三个关键点

  1. 负权边处理:Bellman-Ford算法能够处理图中包含负权边的情况,这与Dijkstra算法不同,后者只适用于非负权边的图。(负权边没问题:即使道路有的收费比别的低(负权边),这个算法也能算。)

  2. 动态规划求解:算法通过动态规划的方法,对所有边进行n-1次松弛操作,其中n是图中节点的数量,以此来迭代更新每个节点的最短路径估计。(多轮更新:一遍遍地检查和更新路径,直到找不出更短的路。

  3. 负权环检测:在进行n次松弛操作后,如果发现任何节点的最短路径估计值发生了变化,这表明图中存在负权环,因为这意味着可以通过不断经过这个环来无限减少路径的总权重。(揪出无限绕圈,能发现那种可以一直绕、一直省钱的环路。)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值