单源最短路算法汇总、分析、整理、实现与简单变形(C++ 语言描述)

一、单源最短路的定义及分类

1. 定义

简而言之,就是从一个点出发,到达图中所有其他的点所经过的最小边权之和。也就是说,假设我们从点 d d d 出发,要到一个图中的点 f f f ,则最短路定义为从 d d d f f f 经过的最小边权和。具体如下图所示。
图1

2. 分类

N N N 为总点数, M M M 为总边数。

算法名称主要思想适用图时间复杂度注意事项
Dijkstra(迪杰斯特拉)枚举每个点,更新已知点到待确定的点的距离稠密图(点少边多)优化后 O ( N log ⁡ N ) O(N \log N) O(NlogN)不能处理负边权
Bellman-Ford(贝尔曼-福特)枚举每条边,对于边上的端点运用上一次的状态更新本次状态稀疏图(点多边少) O ( M 2 ) O(M^2) O(M2)可以处理 k k k 步最短路问题( k k k 为给定的常数)
SPFA类似于 BFS 的处理方法不专门应对 SPFA 算法缺陷的图(例如菊花图) O ( k M )   ( k ≤ N ) O(kM)\text{ }(k \le N) O(kM) (kN)注意看数据范围和图的类型

Dijkstra 的具体讲解请阅读这篇文章,本文着重讲解 Bellman-Ford 和 SPFA 的思路。

二、Bellman-Ford 算法的主要思想和算法实现

1. 主要思想

Bellman-Ford 算法会枚举图中的每一条边,将每条边的顶点信息更新。具体来说,如果记第 i i i 个点距离源点 k k k 的距离为 d i d_i di ,则 Bellman-Ford 算法会先初始化 d i = ∞ d_i = \infin di= d k = 0 d_k = 0 dk=0 ,并对于每个操作枚举与已知距离的点(定义为 d i ≠ ∞ d_i \neq \infin di=),并计算能通过边扩展到的点的最短路(从当前点扩展)。
下面这张图将具体展示 Bellman-Ford 算法的思想,红色边为当前能扩展到的边,红色数字为当前最短路,绿色数字表示被更新的最短路,黑色数字表示顶点的序号或边权。
图2

2. 实现方法

初始化:标记两个数组 f i , g i → ∞ f_i,g_i \rightarrow \infin fi,gi ,其中 f i f_i fi 表示上一步状态, g i g_i gi 表示当前状态。( α \alpha α )
算法过程:进行 M M M 次操作。( β \beta β )
对于每一次操作,先把 f f f 复制一份到 g g g ,相当于在之后的更新中在 f f f 处存档 g g g 上一步的状态。每一次更新时,对于每一个满足 d i ≠ ∞ d_i \neq \infin di= i i i ,扩展点 d i d_i di ,设点 i i i 与点 j j j 相连,则令点 j j j d j d_j dj 值更新为 g j ← min ⁡ { g j , f i + W i , j → } g_j \leftarrow \min\{g_j,f_i+W_{\overrightarrow{i,j}}\} gjmin{gj,fi+Wi,j } γ \gamma γ) 。

3. 关于上面 α \alpha α β \beta β 引理的证明

证明 α \alpha α 的过程如下:
利用反证法求解。
∵ \because 只有一个数组 f f f
∴ \therefore 状态转移方程 γ \gamma γ 应该改为 f j ← min ⁡ { f j , f i + W i , j → } f_j \leftarrow \min\{f_j,f_i+W_{\overrightarrow{i,j}}\} fjmin{fj,fi+Wi,j }
∴ \therefore 这样,若 f i f_i fi 的值已经更改过, f j f_j fj 的值将会是第 2 2 2 步的结果,与其他元素不能同步。
∴ \therefore α \alpha α 得证。

证明 β \beta β 的过程如下:
如 2.1 中图所示,每一次都从原来的已知端点扩展到新的端点,即与边的数量有关,则如果一共有 M M M 条边,扩展次数将会达到 M M M 次。
⋆ \star 注意,在这个位置,我们可以更改循环次数从而使问题转变为 K K K 步的最短路问题。具体见下文的代码。

注意,本部分内容仅代表个人做题经验,可能不严谨,读者若有意见,可以在评论区留言。

4. 代码

K K K 步的 Bellman-Ford 算法,如有需要可以更改为 N N N 步的 Bellman-Ford 算法。

#include <bits/stdc++.h>

using namespace std;

const int N = 1e4 + 10;

struct Node
{
    int x, y, z;
} e[N];

int n, m, k, f[N], g[N];

int main()
{
    cin >> n >> m >> k;
    for (int i = 1; i <= m; ++i)
    {
        cin >> e[i].x >> e[i].y >> e[i].z;
    }

    memset(f, 0x3f, sizeof(f));
    f[1] = 0;
    for (int i = 1; i <= k; ++i)
    {
        memcpy(g, f, sizeof(f));

        for (int j = 1; j <= m; ++j)
        {
            f[e[j].y] = min(f[e[j].y], g[e[j].x] + e[j].z);
        }
    }

    cout << f[n] << endl;

    return 0;
}

三、 SPFA 算法的主要思想、算法实现和注意事项

1. 主要思想

如前文所述,SPFA 算法与 BFS 算法的思想比较接近。在 SPFA 算法的进行过程中,记 d i d_i di 表示到第 i i i 个点的最短路。对于每次操作,枚举当前队列中已经存在的点,以边为依据扩展当前所有已知的点,直到队列为空为止。记当前点为 i i i ,待扩展的点为 j j j ,则状态转移方程可以表示为 d j = min ⁡ { d j , d i + W i , j → } d_j=\min\{d_j,d_i+W_{\overrightarrow {i,j}}\} dj=min{dj,di+Wi,j }
如图 2.1 所示,方法大致与 Bellman-Ford 相同,但每次不会枚举所有的边,而是枚举队列里面的边。

2. 算法实现

初始化:记录一个数组 d i = ∞   ( i ∈ [ 2 , N ] ) d_i=\infin\text{ }(i\in[2,N]) di= (i[2,N]) ,初始化一个队列 queue< int > q
算法过程:开始的时候,先使源点入队,然后记录每个入队的点的位置。
如果当前点 i i i 使扩展点 j j j d j d_j dj 值发生变化,则若队列中不存在点 j j j ,将点 j j j 入队并统计点 j j j 的入队次数 c j c_j cj(后文引理 θ \theta θ 基于此数据完成)。如果 c j ≥ N c_j \ge N cjN ,则判定此图中一定存在负环,故不存在最短路( θ \theta θ) 。

⋆ \star 3. 引理 θ \theta θ 的证明

证明:
∵ \because 如果图中不存在一个环,假设我们有最坏情况(菊花图),有 N − 1 N-1 N1 个点指向中心的点,
∴ \therefore 最多的点需要入队 N − 1 N-1 N1 次。
∵ \because 如果该图中存在一个正环,即环内边权相加为正,则如果绕行圈数增加,最短路增加,
∴ \therefore 正环中的点不会入队超过 N − 1 N-1 N1 次。
∵ \because 负环中的点随着绕行圈数的加大而有更优的最短路,
∴ \therefore 入队次数 ≥ N \ge N N
∴ \therefore θ \theta θ 得证。

4. 代码

判断负环+最短路

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;
const int N = 1e3 + 10;

struct Node
{
    ll to, wi;
};

ll n, m, s, v[N], c[N], d[N];
vector<Node> e[N];
queue<ll> q;

int spfa(int x, int y)
{
    memset(d, 0x3f, sizeof(d));
    d[s] = 0;
    c[s] = 1;

    q.empty();
    for (int i = x; i <= y; ++i)
    {
        v[i] = 1;
        q.push(i);
    }

    while (!q.empty())
    {
        ll x = q.front();
        q.pop();
        v[x] = 0;

        for (Node y : e[x])
        {
            if (d[y.to] > d[x] + y.wi)
            {
                d[y.to] = d[x] + y.wi;

                if (!v[y.to])
                {
                    v[y.to] = 1;
                    c[y.to] = c[x] + 1;
                    
                    if (c[y.to] >= n)
                    {
                        return -1;
                    }

                    q.push(y.to);
                }
            }
        }
    }

    return 1;
}

int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    
    cin >> n >> m >> s;
    for (ll i = 1, a, b, c; i <= m; ++i)
    {
        cin >> a >> b >> c;

        e[a].push_back( Node{b, c} );
    }

    if (spfa(1, n) == -1) 
    {
        cout << -1 << endl;
        return 0;
    }

    spfa(s, s);

    for (ll i = 1; i <= n; ++i)
    {
        if (d[i] > 1e9) cout << "NoPath" << endl;
        else cout << d[i] << endl;
    }

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值