Dijkstra

单源汇最短路,要求图上不存在负权边
时间复杂度为 $O((N+M)logN)$

Important

经暴打,使用优先队列的时间更优,因为常数更小,小心被Set卡常哦

优先队列

int d[N];
int st[N];

using pt = pair<int, int>;

void dij(int s, int u)
{
    for (int i = 1; i <= n; i ++) {
        d[i] = inf;//1e9或1e18  不要0x3f,因为不够ll用
        st[i] = false;
    }
    d[s] = 0;
    priority_queue<pt, vector<pt>, greater<pt>> q;
    q.push({d[s], s});
    while (!q.empty())
    {
        int x = q.top().second;
        q.pop();
        if (st[x])
            continue;
        st[x] = true;
        for (auto [y, v] : edge[x])
            if (d[y] > d[x] + v)
            {
                d[y] = d[x] + v;
                q.push({d[y], y});
            }
    }
}

Set

vector<pair<int, int>> edge[N];

int n, m, s;
int dist[N];

set<pair<int, int>> q;

void dij(int s){
    memset(dist, 127, sizeof dist);
    dist[s] = 0; q.clear();
    for (int i = 1; i <= n; i ++ ) 
        q.insert(make_pair(dist[i], i));
    while (!q.empty()){
        int x = q.begin()->second;
        q.erase(q.begin());
        if (dist[x] > 1 << 30) break;//无法抵达x
        for (auto [y, v]:edge[x])
            if (dist[y] > dist[x] + v){
                q.erase(make_pair(dist[y], y));
                dist[y] = dist[x] + v;
                q.insert(make_pair(dist[y], y));
            }
    }    
}

分层图

点的状态有k种,并且k较小时,可以使用二维 $[x][j]$ 表示 $nk$ 种状态,这样就可以转化为最短路,跑djk
记得改变转移代码
经实验,nk状态(不建立分层图,队列维护3条信息)比nk点(建立分层图,队列维护2信息)更简便易写。
示例如下:

vector<int> edge[N]; //依情况调整

int n, m, k, a[N];

ll d[N][12];
int st[N][12];

using pt = pair<int, int>;
using ptt = pair<ll, pt>;

void dij()
{
    for (int i = 1; i <= n; i ++ ) 
        for (int j = 0; j <= k; j ++ ) {
            d[i][j] = 1e18, st[i][j] = false;
        }
    priority_queue<ptt, vector<ptt>, greater<ptt>> q;
    d[1][0] = a[1];
    q.push({d[1][0], {1, 0}});
    d[1][1] = 1;
    q.push({d[1][1], {1, 1}});
    while (!q.empty())
    {
        auto [x, j] = q.top().second;
        q.pop();
        if (st[x][j])
            continue;
        st[x][j] = true;
        for (auto y : edge[x]){
            if (d[y][0] > d[x][j] + a[y]){
                d[y][0] = d[x][j] + a[y];
                q.push({d[y][0], {y, 0}});
            }
            if (j < k)
                if (d[y][j + 1] > d[x][j] + 1){
                    d[y][j + 1] = d[x][j] + 1;
                    q.push({d[y][j + 1], {y, j + 1}});
                }
        }
    }
}

void solve(){
    cin >> n >> m >> k;
    for (int i = 1; i <= n; i ++ ) cin >> a[i], edge[i].clear();
    while (m -- ){
        int u, v;
        cin >> u >> v;
        edge[u].push_back(v);
        edge[v].push_back(u);
    }
    dij();
    ll ans = 1e18;
    for (int j = 0; j <= k; j ++ ) ans = min(ans, d[n][j]);
    cout << ans << '\n';
}

Bellman-Ford

Bellman–Ford 算法是一种基于松弛(relax)操作的最短路算法,可以求出有负权的图的最短路,并可以对最短路不存在的情况进行判断。
由于每轮松弛会使得最短路的边数至少+1,而最短路的边数最多 $n-1$,故最多进行 $n-1$轮松弛操作。
如果第 $n$ 轮循环时仍然存在可以松弛的边,说明从 $S$ 出发,能够抵达一个负环
时间复杂度为 $O(NM)$

struct Node
{
    int x, y, v;
} edge[M];

int n, m, num = 0;
int dist[N];

bool bf(int s)
{
    memset(dist, 127, sizeof dist);
    dist[s] = 0;
    int cnt = 0;
    for (;;)
    {
        ++cnt;
        bool ok = false;
        for (int i = 1; i <= num; i++)
        {
            auto [x, y, v] = edge[i];
            if (dist[x] < 1 << 30 && dist[y] > dist[x] + v){ //可以到达x && 能松弛 
                dist[y] = dist[x] + v;
                ok = true;
            }
        }
        if (ok && cnt == n) return true;
        if (!ok)
            break;
    }
    return false;
}

Warning

需要注意的是,以 $S$ 点为源点跑Bellman-Ford算法时,如果没有给出存在负环的结果,只能说明从 $S$ 点出发不能抵达一个负环,>不能说明图上不存在负环。

因此需要判断整张图上是否存在负环时,最严谨的做法时设置一个超级源点,向图上每个顶点连一条权值是 $0$ 的边,然后以超级源点为起点跑一遍Bellman-Ford算法

SPFA

Bellman-Ford的优化
很多时候我们并不需要那么多无用的松弛操作。
很显然,只有上一次被松弛的结点,所连接的边,才有可能引起下一次松弛。
那么只需要用队列来维护 引起松弛的结点,就只能访问必要的边了。
SPFA也可以判断 $S$ 能否抵达一个负环,只需要记录最短路经过的边数,但至少为 $n$ 条边时,说明从起点 $S$ 抵达一个负环。
时间复杂度最多(被卡)为 $O(NM)$

vector<pair<int, int>> edge[N];

int n, m, s;
int dist[N], b[N], q[N * M], cnt[N];

bool spfa(int s)
{
    memset(dist, 127, sizeof dist);
    memset(b, false, sizeof b);
    dist[s] = 0;
    b[s] = true;

    int front = 1, rear = 0;
    q[++rear] = s;
    while (front <= rear)
    {
        int x = q[front];
        ++front;
        b[x] = false;
        for (auto [y, v] : edge[x])
            if (dist[y] > dist[x] + v)
            {
                dist[y] = dist[x] + v;
                cnt[y] = cnt[x] + 1;
                if (cnt[y] >= n) return true; //存在负环
                if (!b[y])
                {
                    q[++rear] = y;
                    b[y] = true;
                }
            }
    }
    return false;
}

Floyd

多源汇最短路算法
$f[k][i][j]$ 表示从 $i$ 到 $j$,中间只经过 $1-k$ 的最短路径的长度
考虑转移

二者取 $min$

void solve() {
    //非空间优化
    for (int k = 1; k <= n; k ++ ) //注意k作为第一维
        for (int i = 1; i <= n; i ++ )
            for (int j = 1; j <= n; j ++ )
                f[k][i][j] = min(f[k-1][i][j], f[k-1][i][k] + f[k-1][k][j]);

    //空间优化
    for (int k = 1; k <= n; k++) 
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= n; j++)
                if (f[i][k] < 1 << 30 && f[k][j] < 1 << 30)
                    f[i][j] = min(f[i][j], f[i][k] + f[k][j]);
}