You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

5.1 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

##AcWing 851. spfa求最短路

一、题目描述

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。

请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 impossible

数据保证不存在负权回路。

输入格式 第一行包含整数 nm

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

输出格式 输出一个整数,表示 1 号点到 n 号点的最短距离。

如果路径不存在,则输出 impossible

数据范围 1≤n,m≤10^5, 图中涉及边长绝对值均不超过 10000

输入样例:

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

输出样例:

2

二、spfa算法

spfa算法是 bellman-ford算法的 队列优化算法 的别称,通常用于 求含负权边的单源最短路径,以及判负权环

bellman-ford是一个很傻的算法,因为它一共进行n-1次,每次把每条边都遍历一次,不管是不是变小了,都判断一次 dist[b]=min(dist[b],dist[a]+w),其实,dist[b]如果真的变小,是因为 dist[a]变小了,它得到利益,换句话说就是前驱变小而受益,所以可以采用宽搜来做优化。

关键问题

  • st[]作用
    1. 判断当前的点是否已经加入到队列当中了
    2. 已经加入队列的结点不需要反复的加入到队列
    3. 不使用st数组最终也没有关系,使用可以提升效率
  • spfa算法看上去和Dijstra算法长得有一些像但是其中的意义还是 相差甚远:
    • Dijkstra算法中的st[]保存的是:当前确定了到源点距离最小的点,且一旦确定了最小那么就不可逆
    • spfa算法中的st[]表示当前发生过更新的点,且spfa中的st[]数组 可逆 (在标记为true之后又标记为false)。 顺带一提的是bfs中的st数组记录的是当前已经被遍历过的点
    • Dijkstra算法里使用的是 优先队列, 保存的是当前未确定最小距离的点,目的是快速的取出当前到源点距离最小的点;spfa算法中使用的是队列,目的只是记录一下当前发生过更新的点。
  • bellman-ford算法里最后return -1的判断条件写的是dist[n]>0x3f3f3f3f/5;而spfa算法写的是dist[n]==0x3f3f3f3f;其原因在于bellman-ford算法会遍历所有的边,因此不管是不是和源点连通的边它都会得到更新;但是spfa算法不一样,它相当于采用了bfs,因此遍历到的结点都是与源点连通的,因此如果你要求的n和源点不连通,它不会得到更新,还是保持的0x3f3f3f3f

  • bellman-ford算法可以存在负权回路,是因为其循环的次数是有限制的因此最终不会发生死循环;但是spfa算法不可以,由于用了队列来存储,只要发生了更新就会不断的入队,因此假如有负权回路请你不要用spfa否则会死循环。

  • 由于spfa算法是由bellman-ford算法优化而来,在最坏的情况下时间复杂度和它一样即时间复杂度为 O(nm) ,假如题目时间允许可以直接用spfa算法去解Dijkstra算法的题目。(好像spfa有点小小万能的感觉?)

  • 求负环一般使用spfa算法,方法是用一个cnt数组记录每个点到源点的边数,一个点被更新一次就+1,一旦有点的边数达到了n那就证明存在了负环。

三、C++ 代码

#include <bits/stdc++.h>
const int N = 100010, M = 2 * N;
const int INF = 0x3f3f3f3f;

using namespace std;
int n, m;  // 总点数
int d[N];  // 存储每个点到1号点的最短距离
int st[N]; // 存储每个点是否在队列中

// 邻接表
int e[M], h[N], idx, w[M], ne[M];
void add(int a, int b, int c) {
    e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
// 求1号点到n号点的最短路距离如果从1号点无法走到n号点则返回-1
void spfa(int s) {
    // 初始化距离
    memset(d, 0x3f, sizeof d);
    d[s] = 0;
    queue<int> q;
    q.push(s);
    st[s] = 1;

    while (q.size()) {
        int u = q.front();
        q.pop();
        st[u] = 0;
        for (int i = h[u]; ~i; i = ne[i]) {
            int v = e[i];
            if (d[v] > d[u] + w[i]) {
                d[v] = d[u] + w[i];
                // 如果队列中已存在v则不需要将v重复插入,优化一下
                if (!st[v]) {
                    q.push(v);
                    st[v] = 1;
                }
            }
        }
    }
}

int main() {
    memset(h, -1, sizeof h);
    cin >> n >> m;
    while (m--) {
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c);
    }
    spfa(1);
    if (d[n] == INF)
        puts("impossible");
    else
        printf("%d\n", d[n]);
    return 0;
}