Bellman-Ford算法
适用范围:单源最短路,无负环,可以有负权。
记从起点
s
s
s出发到节点
i
i
i的最短距离为
d
i
d_i
di,则下述等式成立。
d
i
=
min
e
=
(
j
,
i
)
∈
E
{
d
j
+
l
e
n
(
j
,
i
)
}
d_i = \min_{e = (j, i)\in E}\{d_j + len(j, i)\}
di=e=(j,i)∈Emin{dj+len(j,i)}
设初值
d
s
=
0
d_s = 0
ds=0,
d
i
=
inf
d_i = \inf
di=inf,不断利用这条递推关系式更新
d
d
d的值,就可以算出新的
d
d
d。
只要原图中不存在负环,更新的次数就是有限的。
时间复杂度: O ( ∣ V ∣ × ∣ E ∣ ) O(|V| \times|E|) O(∣V∣×∣E∣)。
空间复杂度: O ( ∣ V ∣ + ∣ E ∣ ) O(|V| + |E|) O(∣V∣+∣E∣)。
代码
int n, m, s;
LL d[N];
vector<Edge> e[N];
void Bellman_Ford(){
rep1(i, n + 1) d[i] = inf;
d[s] = 0;
while (true){
bool update = false;
rep1(i, n + 1) if (d[i] != inf){
for (Edge u : e[i]) if (d[i] + u.len < d[u.to]){
d[u.to] = d[i] + u.len;
update = true;
}
}
if (!update) break;
}
}
判负环
由于最短路最多只会经过 ∣ V ∣ − 1 |V| - 1 ∣V∣−1条边,所以上述更新最多进行 ∣ V ∣ − 1 |V| - 1 ∣V∣−1轮。
因此如果在第 ∣ V ∣ |V| ∣V∣轮依然更新了 d d d的话,就说明存在负环。
初始化时将所有的 d i d_i di赋为 0 0 0,据说这样可以求出所有负环(然而我还不会qwq)。
代码
int n, m, d[N];
vector<Edge> e[N];
bool find_negative_loop(){
rep1(i, n + 1) d[i] = 0;
rep(cnt, n){
bool update = false;
rep1(i, n + 1) for (Edge u : e[i]) if (d[i] + u.len < d[u.to]){
d[u.to] = d[i] + u.len;
update = true;
if (cnt == n - 1) return true;
}
if (!update) break;
}
return false;
}
SPFA
很容易看出只有被松弛的点才有可能去松弛其他点,所以用一个队列存储一下哪些点被松弛了。
int n, m, s;
LL d[N];
vector<Edge> e[N];
bool in_queue[N];
void SPFA(){
rep1(i, n + 1) d[i] = inf, in_queue[i] = false;
d[s] = 0;
queue<int> que;
que.push(s);
in_queue[s] = true;
while (!que.empty()){
int p = que.front(); que.pop(); in_queue[p] = false;
for (Edge u : e[p]) if (d[p] + u.len < d[u.to]){
d[u.to] = d[p] + u.len;
if (!in_queue[u.to]){
que.push(u.to);
in_queue[u.to] = true;
}
}
}
}
Dijkstra算法
D i j k s t r a Dijkstra Dijkstra适用于不存在负权边的情况。可以对算法进行如下修改。
- 对于找到最短距离的点,从它出发更新相邻节点的距离。
- 此后不需要再关心已经确定最短距离的点。
使用 p r i o r i t y _ q u e u e priority\_queue priority_queue优化,每次取出当前未确定最短距离的点中最近的点,更新从它出发的相邻节点。
时间复杂度: O ( ∣ E ∣ log ∣ V ∣ ) O(|E|\log|V|) O(∣E∣log∣V∣)。
空间复杂度: O ( ∣ V ∣ + ∣ E ∣ ) O(|V| + |E|) O(∣V∣+∣E∣)。
代码
int n, m, s;
LL d[N];
vector<Edge> e[N];
void Dijkstra(){
rep1(i, n + 1) d[i] = inf;
d[s] = 0;
priority_queue<pair<LL, int> > pq;
pq.push(MP(-d[s], s));
while (!pq.empty()){
int cur = pq.top().second;
LL curdis = -pq.top().first;
pq.pop();
if (curdis > d[cur]) continue;
for (Edge u : e[cur]) if (curdis + u.len < d[u.to]){
d[u.to] = curdis + u.len;
pq.push(MP(-d[u.to], u.to));
}
}
}
Floyd-Warshall算法
F l o y d Floyd Floyd用于求任意两点之间的最短路。
记 d p ( k , i , j ) dp(k, i, j) dp(k,i,j)表示只经过点 1 ∼ k 1\sim k 1∼k和 i , j i, j i,j的情况下 i i i到 j j j的最短距离,初始 d p ( 0 , i , j ) = l e n ( i , j ) dp(0, i, j) = len(i, j) dp(0,i,j)=len(i,j),如果不存在从 i i i到 j j j的边则 d p ( 0 , i , j ) = inf dp(0, i, j) = \inf dp(0,i,j)=inf。
分两种情况讨论。
- 最短路不经过 k k k,则 d p ( k , i , j ) = d p ( k − 1 , i , j ) dp(k, i, j) = dp(k - 1, i, j) dp(k,i,j)=dp(k−1,i,j)。
- 最短路经过 k k k,则 d p ( k , i , j ) = d p ( k − 1 , i , k ) + d p ( k − 1 , k , j ) dp(k, i, j) = dp(k - 1, i, k) + dp(k - 1, k, j) dp(k,i,j)=dp(k−1,i,k)+dp(k−1,k,j)。
于是得到 d p ( k , i , j ) = min { d p ( k − 1 , i , j ) , d p ( k − 1 , i , k ) + d p ( k − 1 , k , j ) } dp(k, i, j) = \min\{dp(k - 1, i, j), dp(k - 1, i, k) + dp(k - 1, k, j)\} dp(k,i,j)=min{dp(k−1,i,j),dp(k−1,i,k)+dp(k−1,k,j)}。
同时,可以使用滚动数组来优化空间,可以转化为 d p ( i , j ) = min { d p ( i , j ) , d p ( i , k ) + d p ( k , j ) } dp(i, j) = \min\{dp(i, j), dp(i, k) + dp(k, j)\} dp(i,j)=min{dp(i,j),dp(i,k)+dp(k,j)}。
时间复杂度: O ( ∣ V ∣ 3 ) O(|V|^3) O(∣V∣3)。
空间复杂度: O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2)。
代码
int n, m;
LL d[N][N];
vector<Edge> e[N];
void Floyd(){
rep1(i, n + 1) rep1(j, n + 1) d[i][j] = inf;
rep1(i, n + 1) for (Edge u : e[i]) d[i][u.to] = min(d[i][u.to], u.len); // 防重边
rep1(i, n + 1) d[i][i] = 0;
rep1(k, n + 1) rep1(i, n + 1) rep1(j, n + 1)
if (d[i][k] != inf && d[k][j] != inf) // 防负权边
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}
Johnson算法
J o h n s o n Johnson Johnson是全源最短路算法,相比于 F l o y d Floyd Floyd,它有更高的代码难度但是快很多的效率。在时间允许的情况下,还是建议使用 F l o y d Floyd Floyd算法。
首先注意到如果图中无负权边,那么我们可以枚举每个点作为起点跑 V V V次 D i j k s t r a Dijkstra Dijkstra,从而得到两两之间的最短路,时间复杂度为 O ( ∣ V ∣ ∣ E ∣ log ∣ V ∣ ) O(|V||E|\log|V|) O(∣V∣∣E∣log∣V∣)。可是如果原图中有负权边该怎么办,这就将问题转化为了如何消除图中的负权边。
我们设一个虚拟节点( 0 0 0号节点),从它向每个节点 i i i( 1 ≤ i ≤ V 1 \le i \le V 1≤i≤V)连一条权值为 0 0 0的单向边。定义一个点的势 h i h_i hi为它到 0 0 0号节点的最短距离,可以从 0 0 0号节点 S P F A SPFA SPFA求出所有点的势。
这时,我们重新定义一条从 u u u到 v v v权值为 w w w的边的长度为 w + h u − h v w + h_u - h_v w+hu−hv。由于有三边形不等式 h u + w ≥ h v h_u + w \ge h_v hu+w≥hv,可得 w + h u − h v ≥ 0 w + h_u - h_v \ge 0 w+hu−hv≥0,所以所有的边权都是非负的。
来看一下,重新定义边权后,
s
→
t
s \to t
s→t的最短距离为
(
w
1
+
h
s
−
h
v
1
)
+
(
w
2
+
h
v
1
−
h
v
2
)
+
⋯
+
(
w
n
+
h
v
n
−
1
−
h
t
)
=
w
1
+
w
2
+
⋯
+
w
n
⏟
o
r
i
g
i
n
a
l
s
h
o
r
t
e
s
t
p
a
t
h
+
h
s
−
h
t
\begin{aligned} (&w_1 + h_s - h_{v_1}) + (w_2 + h_{v_1} - h_{v_2}) + \cdots + (w_n + h_{v_{n - 1}} - h_t)\\ =~&\underbrace{w_1 +w_2 + \cdots + w_n}_{original~shortest~path} + h_s - h_t \end{aligned}
(= w1+hs−hv1)+(w2+hv1−hv2)+⋯+(wn+hvn−1−ht)original shortest path
w1+w2+⋯+wn+hs−ht
由于
h
s
−
h
t
h_s - h_t
hs−ht为一个定值,所以我们求得的就是
s
→
t
s\to t
s→t的最短路,最后统一减去
h
s
−
h
t
h_s - h_t
hs−ht就行了。
时间复杂度: O ( ∣ V ∣ ∣ E ∣ log ∣ V ∣ ) O(|V||E|\log|V|) O(∣V∣∣E∣log∣V∣)。
空间复杂度: O ( ∣ V ∣ 2 + ∣ E ∣ ) O(|V|^2 + |E|) O(∣V∣2+∣E∣)。
代码
int n, m;
LL dist[N][N];
vector<Edge> e[N];
LL h[N];
bool in_queue[N];
void Johnson(){
rep1(i, n + 1) e[0].PB(Edge{i, 0});
rep(i, n + 1) h[i] = inf;
h[0] = 0;
queue<int> Q;
Q.push(0);
in_queue[0] = true;
while (!Q.empty()){
int p = Q.front(); Q.pop(); in_queue[p] = false;
for (Edge u : e[p]) if (h[p] + u.len < h[u.to]){
h[u.to] = h[p] + u.len;
if (!in_queue[u.to]){
Q.push(u.to);
in_queue[u.to] = true;
}
}
}
priority_queue<pair<LL, int> > pq;
rep1(s, n + 1){
rep1(i, n + 1) dist[s][i] = inf;
dist[s][s] = 0;
pq.push(MP(-dist[s][s], s));
while (!pq.empty()){
int cur = pq.top().second;
LL curdis = -pq.top().first;
pq.pop();
if (curdis > dist[s][cur]) continue;
for (Edge u : e[cur])
if (curdis + u.len + h[cur] - h[u.to] < dist[s][u.to]){
dist[s][u.to] = curdis + u.len + h[cur] - h[u.to];
pq.push(MP(-dist[s][u.to], u.to));
}
}
rep1(i, n + 1) dist[s][i] -= h[s] - h[i]; // 默认图是联通的,如不联通,减完后可能不是inf
}
}