Acwing Bellman-Ford &SPFA

1. Bellman-Ford

该算法适用于有负权边的情况,注意:如果有负权环的话,最短路就不一定存在了。时间复杂度 O ( m n ) . O(mn). O(mn).该算法可以求出来图中是否存在负权回路,但求解负权回路,通常用SPFA算法,而不用Bell-Ford算法,因为前者的时间复杂度更低。

Bellman-ford不要求用邻接表或者邻接矩阵存储边,为简化操作,可以定义一个结构体,存储a,b,w。表示存在一条边a点指向b点,权重为w。则遍历所有边时,只要遍历全部的结构体数组即可

主要步骤:

  • 循环n次:循环的次数的含义:假设循环了k次则表示:从起点经过不超过k条边,走到某个点的最短距离
  • 每次循环,遍历图中的所有的边。对每条边(a,b,w),(指的是从a点到b点,权值是w的一条边)更新d[b] = min(d[b],d[a]+w)。该操作称为松弛操作。
  • 该算法能够保证,在循环n次后,对所有的边(a,b,w),都满足d[b] <= d[a] + w。这个不等式被称为三角不等式。

先给出板子:

// 定义一个结构体存储边的信息
struct Edge {
    int a, b, w; // a: 起点, b: 终点, w: 边的权重
} edges[M]; // 存储所有的边,最多 M 条

// Bellman-Ford 算法的核心函数,返回 1 到 n 的最短距离
int bellman_ford() {
    // 初始化距离数组,将所有点的距离设置为一个非常大的值(无穷大)
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0; // 源点(起点)1到自己的距离为 0
    
    // Bellman-Ford 算法允许最多使用 k 条边来放松所有边
    for (int i = 0; i < k; i++) { // 执行 k 轮松弛操作
        memcpy(backup, dist, sizeof dist); // 将当前距离备份
        
        // 遍历每条边,尝试更新目标顶点的最短距离
        for (int j = 0; j < m; j++) {
            int a = edges[j].a, b = edges[j].b, w = edges[j].w; // 获取边的起点,终点和权重
            dist[b] = min(dist[b], backup[a] + w); // 更新顶点 b 的距离
        }
    }
    
    // 如果最终 dist[n] 的值仍然非常大,说明无法在 k 条边以内到达顶点 n
    if (dist[n] > 0x3f3f3f3f / 2) return -1; // 0x3f3f3f3f 是表示无穷大的近似值
    return dist[n]; // 返回最短路径距离
}

ACwing 853. 有边数限制的最短路
在这里插入图片描述

实现思路:

  • 利用上述的Bellman-ford算法
  • 依旧定义一个距离数组dist,初始化未正无穷(0x3f3f3f3f)。注意最后判断到n号节点是否有路径不是直接判断dist[n] == 0x3f3f3f3f,因为存在负权边,可能更新的时候会存在dist[n] = 0x3f3f3f3f - c,即无穷大加上一个负数,仍为无穷大但数值还是改变了,所以最后有路径的判断改为dist[n] > 0x3f3f3f3f / 2.
  • 本题要求1号到n号点不超过k条边的最短距离,则循环k次来寻找最短路;
  • 每次再遍历m条边,判断加入当前点后,各店点到起点的距离是否变小,若变小则更新距离;
  • 注意:该距离更新时可能会导致参与的边的数量大于k,因此应在每次遍历前设置一个备份数组backup,记录在k次遍历中,本次遍历的上一次的距离数组状态,在该次遍历中对每条边的距离数组更新时采用备份数组,确保本次更新范围在当前的边数限制内。

具体实现代码:`

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 510, M = 10010;

int n, m, k; // n: 顶点数, m: 边数, k: 最多使用的边数
int dist[N], backup[N]; // dist: 存储当前节点的最短距离,backup: 每轮备份上一轮的最短距离

// 定义一个结构体存储边的信息
struct Edge {
    int a, b, w; // a: 起点, b: 终点, w: 边的权重
} edges[M]; // 存储所有的边,最多 M 条

// Bellman-Ford 算法的核心函数,返回 1 到 n 的最短距离
int bellman_ford() {
    // 初始化距离数组,将所有点的距离设置为一个非常大的值(无穷大)
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0; // 源点(起点)1到自己的距离为 0
    
    // Bellman-Ford 算法允许最多使用 k 条边来放松所有边
    for (int i = 0; i < k; i++) { // 执行 k 轮松弛操作
        memcpy(backup, dist, sizeof dist); // 将当前距离备份
        
        // 遍历每条边,尝试更新目标顶点的最短距离
        for (int j = 0; j < m; j++) {
            int a = edges[j].a, b = edges[j].b, w = edges[j].w; // 获取边的起点,终点和权重
            dist[b] = min(dist[b], backup[a] + w); // 更新顶点 b 的距离
        }
    }
    
    // 如果最终 dist[n] 的值仍然非常大,说明无法在 k 条边以内到达顶点 n
    if (dist[n] > 0x3f3f3f3f / 2) return -1; // 0x3f3f3f3f 是表示无穷大的近似值
    return dist[n]; // 返回最短路径距离
}

int main() {
    cin >> n >> m >> k; // 读取顶点数 n, 边数 m 和最多可用边数 k
    
    // 读取每条边的起点、终点和权重,并存入 edges 数组
    for (int i = 0; i < m; i++) {
        int a, b, w;
        cin >> a >> b >> w;
        edges[i] = {a, b, w};
    }
    
    // 调用 bellman_ford 函数计算最短路径
    int t = bellman_ford();
    
    // 如果 t 返回 -1 且 dist[n] 不是 -1(没有负权环),输出 "impossible"
    if (t == -1 && dist[n] != -1) puts("impossible"); 
    else cout << t << endl; // 否则输出最短路径的距离
    
    return 0;
}

2.SPFA

  • 若要使用SPFA算法,一定要求图中不能有负权回路。只要图中没有负权回路,都可以用SPFA,即也可以求解正权边的题,这个算法的限制是比较小的。时间复杂度一般为 O ( m ) , O(m), O(m)最差为 O ( m n ) . O(mn). O(mn).在一些情况下可以代替Dijkstra算法

  • SPFA其实是对Bellman-Ford的一种优化,相比Bellman-Ford判环的时间复杂度也更低。 它优化的是这一步:d[b] =
    min (d[b],d[a] + w)

  • 我们观察可以发现,只有当d[a]变小了,在下一轮循环中以a为起点的点(或者说a的出边)就会更新,即下一轮循环必定更新d[b]。

  • 考虑用一个队列queue,来存放距离变小的节点(当图中存在负权回路时,队列永远都不会为空,因为总是存在某个点,在一次松弛操作后,距离变小)。

板子:

int e[N], ne[N], idx, w[N], h[N]; // e: 邻接点, ne: 下一条边的索引, 
//idx: 当前边的索引, w: 边的权重, h: 头节点

int dist[N]; // 存储从起点 1 到每个节点的最短距离
int n, m; 
bool s[N]; // 记录节点是否在队列中,避免重复入队

// 添加一条从 a 到 b 的边,权重为 c 的边
void add(int a, int b, int c) {
    e[idx] = b; // 终点 b
    ne[idx] = h[a]; // 将边 idx 加到节点 a 的邻接表中
    w[idx] = c; // 边的权重
    h[a] = idx++; // 更新节点 a 的头指针,指向新加的边
}

// SPFA 算法求解最短路径
int spfa() {
    memset(dist, 0x3f, sizeof dist); // 初始化距离为无穷大
    dist[1] = 0; // 起点 1 到自己的距离为 0
    queue<int> q; // 定义队列用于处理节点
    q.push(1); // 将起点 1 入队
    s[1] = true; // 标记起点 1 已入队

    // 队列不为空时进行循环
    while (q.size()) {
        auto t = q.front(); // 取出队首节点
        q.pop(); // 弹出队首节点
        s[t] = false; // 标记节点 t 不在队列中

        // 遍历节点 t 的所有邻接边
        for (int i = h[t]; i != -1; i = ne[i]) {
            int j = e[i]; // 获取节点 t 的邻接点 j
            // 如果从节点 t 到 j 的路径更短,则更新 j 的距离
            if (dist[j] > dist[t] + w[i]) {
                dist[j] = dist[t] + w[i];
                // 如果节点 j 不在队列中,则将其加入队列
                if (!s[j]) {
                    s[j] = true; // 标记节点 j 已入队
                    q.push(j); // 节点 j 入队
                }
            }
        }
    }

    // 如果终点 n 的距离仍然为无穷大,表示无法到达,返回 -1
    if (dist[n] > 0x3f3f3f3f / 2) return -1;
    else return dist[n]; // 否则返回最短距离
}

具体实现代码(详解版):

#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>

using namespace std;

const int N = 100010;

int e[N], ne[N], idx, w[N], h[N]; // e: 邻接点, ne: 下一条边的索引, 
//idx: 当前边的索引, w: 边的权重, h: 头节点

int dist[N]; // 存储从起点 1 到每个节点的最短距离
int n, m; 
bool s[N]; // 记录节点是否在队列中,避免重复入队

// 添加一条从 a 到 b 的边,权重为 c 的边
void add(int a, int b, int c) {
    e[idx] = b; // 终点 b
    ne[idx] = h[a]; // 将边 idx 加到节点 a 的邻接表中
    w[idx] = c; // 边的权重
    h[a] = idx++; // 更新节点 a 的头指针,指向新加的边
}

// SPFA 算法求解最短路径
int spfa() {
    memset(dist, 0x3f, sizeof dist); // 初始化距离为无穷大
    dist[1] = 0; // 起点 1 到自己的距离为 0
    queue<int> q; // 定义队列用于处理节点
    q.push(1); // 将起点 1 入队
    s[1] = true; // 标记起点 1 已入队

    // 队列不为空时进行循环
    while (q.size()) {
        auto t = q.front(); // 取出队首节点
        q.pop(); // 弹出队首节点
        s[t] = false; // 标记节点 t 不在队列中

        // 遍历节点 t 的所有邻接边
        for (int i = h[t]; i != -1; i = ne[i]) {
            int j = e[i]; // 获取节点 t 的邻接点 j
            // 如果从节点 t 到 j 的路径更短,则更新 j 的距离
            if (dist[j] > dist[t] + w[i]) {
                dist[j] = dist[t] + w[i];
                // 如果节点 j 不在队列中,则将其加入队列
                if (!s[j]) {
                    s[j] = true; // 标记节点 j 已入队
                    q.push(j); // 节点 j 入队
                }
            }
        }
    }

    // 如果终点 n 的距离仍然为无穷大,表示无法到达,返回 -1
    if (dist[n] > 0x3f3f3f3f / 2) return -1;
    else return dist[n]; // 否则返回最短距离
}

int main() {
    cin >> n >> m; // 输入节点数 n 和边数 m
    memset(h, -1, sizeof h); // 初始化头节点数组为 -1,表示没有边

    // 读取每条边的信息,并调用 add 函数将其加入邻接表
    while (m--) {
        int a, b, w;
        cin >> a >> b >> w;
        add(a, b, w);
    }

    // 调用 spfa 函数计算最短路径
    int t = spfa();
    
    // 如果最短距离为 -1,且终点距离不为 -1,则输出 "impossible"
    if (t == -1 && dist[n] != -1) puts("impossible");
    else cout << t << endl; // 否则输出最短路径的距离

    return 0;
}

下面给出SPAF算法的另一个应用,可以判断有向图中是否存在负权回路

在这里插入图片描述
实现思路:

  • 在以上SPFA的基础上,设置一个记录边数的数组cnt[],即代表起点到当前点经过的边数
  • 在每次更新某点到起点最小距离的同时,将该点的cnt数组值加一,即经过的边数多了一条,当该数组的值大于n,则说明存在负环(n个点若不存在环,最多只有n-1条边)
  • 注意:由于判断的不是起点开始存在负环,而是全部图中是否存在负环,因此初始要将所有点都加入队列当中

具体实现代码(详解版):

#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>

using namespace std;
const int N = 100010;

int e[N], ne[N], w[N], h[N], idx; // e: 邻接点, ne: 下一条边的索引, w: 边的权重, h: 头节点, idx: 当前边的索引
int dist[N], cnt[N]; // dist: 从起点到每个点的最短距离, cnt: 记录从起点到当前点的边数
int n, m; // n: 节点数, m: 边数
bool s[N]; // 记录节点是否在队列中,防止重复入队

// 添加一条从 a 到 b 的边,权重为 c
void add(int a, int b, int c) {
    e[idx] = b; // 邻接点为 b
    ne[idx] = h[a]; // 边的下一条边的索引
    w[idx] = c; // 边的权重为 c
    h[a] = idx++; // 更新头节点 h[a]
}

// SPFA 算法,返回是否存在负权回路
bool spfa() {
    memset(dist, 0x3f, sizeof dist); // 初始化距离为无穷大
    dist[1] = 0; // 起点 1 到自己的距离为 0
    queue<int> q; // 定义队列

    // 初始化,将所有节点都放入队列中
    for (int i = 1; i <= n; i++) {
        q.push(i);
        s[i] = true; // 标记节点 i 已入队
    }

    // 队列不为空时循环处理
    while (q.size()) {
        int t = q.front(); // 取出队首元素
        q.pop(); // 弹出队首元素
        s[t] = false; // 标记节点 t 不在队列中

        // 遍历节点 t 的所有邻接边
        for (int i = h[t]; i != -1; i = ne[i]) {
            int j = e[i]; // 终点 j
            // 如果从 t 到 j 的路径更短,则更新距离
            if (dist[j] > dist[t] + w[i]) {
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1; // 更新从起点到 j 的边数

                // 如果边数大于等于 n,说明存在负环
                if (cnt[j] >= n) return true;
                
                // 如果节点 j 不在队列中,则将其加入队列
                if (!s[j]) {
                    s[j] = true; // 标记节点 j 已入队
                    q.push(j); // 将 j 入队
                }
            }
        }
    }

    return false; // 如果没有检测到负环,返回 false
}

int main() {
    cin >> n >> m; // 输入节点数 n 和边数 m
    memset(h, -1, sizeof h); // 初始化邻接表头节点数组为 -1,表示没有边
    // 读取每条边的信息,并调用 add 函数将其加入邻接表
    while (m--) {
        int a, b, w;
        cin >> a >> b >> w;
        add(a, b, w);
    }

    // 调用 spfa 函数,如果存在负权回路,输出 "Yes",否则输出 "No"
    if (spfa()) cout << "Yes";
    else cout << "No";
    
    return 0;
}

下面总结一下以上两种算法:
在这里插入图片描述
适用场景

  • Bellman-Ford:当你需要计算图中存在负权边的最短路径,并且对所有边进行遍历时效果较好。适合图不太稀疏的情况。
  • SPFA:是一种优化版,通常在稀疏图中表现更好。适用于解决含负权边的单源最短路径问题,特别是当图中路径较短、图较稀疏时表现尤为优异。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值