题解:AcWing 851 SPFA求最短路

【题目来源】

AcWing:851 spfa求最短路 - AcWing题库

【题目描述】

给定一个\(n\)个点\(m\)条边的有向图,图中可能存在重边和自环,边权可能为负数。请你求出\(1\)号点到\(n\)号点的最短距离,如果无法从\(1\)号点走到\(n\)号点,则输出 impossible。数据保证不存在负权回路。

【输入】

第一行包含整数\(n\)\(m\)。接下来\(m\)行每行包含三个整数 \(x,y,z\),表示存在一条从点\(x\)到点\(y\)的有向边,边长为\(z\)

【输出】

输出一个整数,表示\(1\)号点到\(n\)号点的最短距离。如果路径不存在,则输出 impossible。

【输入样例】

3 3
1 2 5
2 3 -3
1 3 4

【输出样例】

2

【核心思想】

  1. 问题分析:给定有向图 \(G=(V,E)\),其中 \(|V|=n\)\(|E|=m\),边权可能为负数,但保证不存在负权回路。需要求从起点 \(1\) 到终点 \(n\) 的最短距离。这是一个带负权边的单源最短路径问题,由于边权可能为负,不能使用 Dijkstra 算法,而 SPFA 是 Bellman-Ford 算法的队列优化版本,效率更高。

  2. 算法选择

    • SPFA 算法(Shortest Path Faster Algorithm):Bellman-Ford 算法的队列优化版本,只松弛那些距离被更新过的节点的邻接边
    • 距离数组 \(dist[]\)\(dist[i]\) 表示从起点到节点 \(i\) 的当前最短距离估计值
    • 队列 \(q\):存储待松弛的节点,只有距离被更新的节点才会入队
    • 标记数组 \(st[]\):标记节点是否在队列中,避免重复入队
    • 邻接表存储:使用链式前向星存储图,空间复杂度 \(O(n + m)\)
  3. 关键步骤

    • 初始化
      • 读取 \(n\)(节点数)、\(m\)(边数)
      • 初始化邻接表头数组 \(h[i] = -1\),边索引 \(idx = 0\)
      • 读入 \(m\) 条边 \((x, y, z)\),使用 add(x, y, z) 添加到邻接表
      • 初始化距离数组 \(dist[i] = INF\)\(dist[1] = 0\)(起点距离为0)
      • 初始化标记数组 \(st[i] = false\)
    • 起点入队q.push(1)st[1] = true
    • 主循环(当队列非空时循环):
      • 取出队首t = q.front(),然后 q.pop()st[t] = false
      • 松弛邻接边:遍历 \(t\) 的所有邻接边 \((t \to j,权重w)\)
        • \(dist[j] > dist[t] + w\),则更新 \(dist[j] = dist[t] + w\)
        • \(j\) 不在队列中(\(!st[j]\)),则将 \(j\) 入队 q.push(j)st[j] = true
    • 返回结果
      • \(dist[n] == INF\),说明不可达,输出 impossible
      • 否则输出 \(dist[n]\) 作为答案
  4. 时间/空间复杂度

    • 时间复杂度:平均 \(O(m)\),最坏 \(O(n \cdot m)\)(存在负权边时)
    • 空间复杂度:\(O(n + m)\),邻接表 \(O(n + m)\),距离数组、标记数组、队列各 \(O(n)\)
    • 适用于带负权边的单源最短路径问题,在稀疏图上效率很高
  5. SPFA 算法的核心思想

    • 松弛操作:只有当某个节点的距离被更新时,它的邻接边才可能需要松弛,因此只将这些节点加入队列
    • 队列优化:相比 Bellman-Ford 的盲目遍历所有边,SPFA 只处理可能产生更新的边,大幅减少无效操作
    • 惰性更新:节点出队后才进行松弛,避免重复处理
    • 负权边处理:可以正确处理负权边,但无法处理负权回路(需要额外判断)
    • 适用场景:稀疏图、带负权边的单源最短路径问题
    • 在随机数据上效率很高,但可以被特殊数据卡到最坏情况

【解题思路】

【解题思路2】

【算法标签】

SPFA

【代码详解】

#include <bits/stdc++.h>
using namespace std;

// 定义常量
const int N = 100010, M = N, INF = 1e9; // N为节点数上限,M为边数上限,INF为无穷大

// 定义邻接表相关数组
int h[N], e[M], w[M], ne[M], idx; // h存储每个节点的第一条边的索引,e存储边的终点,w存储边的权重,ne存储下一条边的索引,idx为边的计数器
int dist[N]; // dist存储从起点到每个节点的最短距离
queue<int> q; // 队列用于广度优先搜索
bool st[N]; // st标记节点是否在队列中

// 定义节点数和边数
int n, m;

// 添加边的函数
void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c; // 设置边的终点和权重
    ne[idx] = h[a], h[a] = idx++; // 更新邻接表
}

// SPFA算法实现
int spfa() {
    fill(dist, dist + N, INF); // 初始化距离为无穷大
    dist[1] = 0; // 起点距离设为0
    q.push(1); // 将起点加入队列
    st[1] = true; // 标记起点在队列中
    while (!q.empty()) {
        int nd = q.front(); q.pop(); // 取出队首元素
        st[nd] = false; // 标记该节点不在队列中
        for (int i = h[nd]; i != -1; i = ne[i]) { // 遍历所有邻接边
            int j = e[i]; // 邻接节点
            if (dist[j] > dist[nd] + w[i]) { // 如果通过当前节点到达邻接节点更近
                dist[j] = dist[nd] + w[i]; // 更新最短距离
                if (!st[j]) { // 如果邻接节点不在队列中
                    q.push(j); // 将邻接节点加入队列
                    st[j] = true; // 标记邻接节点在队列中
                }
            }
        }
    }
    return dist[n]; // 返回从起点到终点的最短距离
}

// 主函数
int main() {
    cin >> n >> m; // 输入节点数和边数
    memset(h, -1, sizeof(h)); // 初始化邻接表头指针为-1
    while (m--) { // 读取所有边的信息
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c); // 添加边
    }
    int ans = spfa(); // 计算最短路径
    if (ans == INF) cout << "impossible"; // 如果无法到达终点,输出"impossible"
    else cout << dist[n]; // 否则输出最短距离
    return 0;
}

#include <bits/stdc++.h>
using namespace std;

typedef pair<int, int> PII;  // 存储距离和节点编号的对
const int N = 100010;
int n, m;  // n: 节点数,m: 边数
int h[N], w[N], e[N], ne[N], idx;  // 邻接表存储图
int dist[N];  // 存储从起点到每个点的最短距离
bool st[N];  // 标记节点是否在队列中

// 添加一条有向边 a->b,权重为c
void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

// SPFA算法求从节点1到节点n的最短路径
int spfa()
{
    memset(dist, 0x3f, sizeof(dist));  // 初始化距离为无穷大
    dist[1] = 0;  // 起点到自己的距离为0

    queue<int> q;  // 队列存储需要松弛的节点
    q.push(1);  // 起点入队
    st[1] = true;  // 标记起点在队列中

    while (q.size())  // 队列不为空时循环
    {
        int t = q.front();  // 取出队首节点
        q.pop();
        st[t] = false;  // 标记节点已出队

        // 遍历节点t的所有邻接节点
        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];  // 邻接节点编号
            // 如果通过t到j的距离更短,更新距离
            if (dist[j] > dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                // 如果j不在队列中,将其加入队列
                if (!st[j])
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }
    return dist[n];  // 返回从起点到节点n的最短距离
}

int main()
{
    scanf("%d%d", &n, &m);  // 读入节点数和边数
    memset(h, -1, sizeof(h));  // 初始化邻接表头指针为-1

    while (m--)
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);  // 添加有向边
    }

    int t = spfa();  // 执行SPFA算法

    if (t == 0x3f3f3f3f)  // 如果距离为无穷大,说明不可达
    {
        puts("impossible");
    }
    else
    {
        printf("%d\n", t);  // 输出最短距离
    }

    return 0;
}

【运行结果】

3 3
1 2 5
2 3 -3
1 3 4
2
posted @ 2026-06-27 14:20  团爸讲算法  阅读(6)  评论(0)    收藏  举报