一、单源最短路的定义及分类
1. 定义
简而言之,就是从一个点出发,到达图中所有其他的点所经过的最小边权之和。也就是说,假设我们从点
d
d
d 出发,要到一个图中的点
f
f
f ,则最短路定义为从
d
d
d 到
f
f
f 经过的最小边权和。具体如下图所示。
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) (k≤N) | 注意看数据范围和图的类型 |
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. 实现方法
初始化:标记两个数组
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}}\}
gj←min{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}}\}
fj←min{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
cj≥N ,则判定此图中一定存在负环,故不存在最短路(
θ
\theta
θ) 。
⋆ \star ⋆ 3. 引理 θ \theta θ 的证明
证明:
∵
\because
∵ 如果图中不存在一个环,假设我们有最坏情况(菊花图),有
N
−
1
N-1
N−1 个点指向中心的点,
∴
\therefore
∴ 最多的点需要入队
N
−
1
N-1
N−1 次。
∵
\because
∵ 如果该图中存在一个正环,即环内边权相加为正,则如果绕行圈数增加,最短路增加,
∴
\therefore
∴ 正环中的点不会入队超过
N
−
1
N-1
N−1 次。
∵
\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;
}