图的最短路
一、定义
从图中某一顶点 (源点) 出发,到达另一顶点 (终点) 的所有路径中 (路径可能不存在或者存在不止一条) , 各边权值之和最小的路径,称为最短路径;
最短路径问题是图论的经典问题;
最短路径问题分为两类,
- 求单个顶点和其他所有顶点的最短路径,称为单源最短路径问题 ;
- 求所有顶点相互之间的最短路径,称为多源最短路径问题 ;
对于以上两类最短路径问题,都有相应的有效算法予以解决。
二、Floyd 算法
1. Floyd 算法
Floyd 算法又称为插点法,是一种用于寻找给定的加权图中多源点之间最短路径的算法;
2. 特点
Floyd 算法用于计算多元最短路,可计算出现负边权时的最短路,实际应用中,很多题目不是求如何用 Floyd 求最短路,而是用 Floyd 的动态规划思想来解决类似 Floyd 的问题;
3. 实现
Floyd 算法基于动态规划方法,使用 dp 存放当前顶点之间的最短路径长度;
状态
d p [ k ] [ i ] [ j ] dp[k][i][j] dp[k][i][j] 为前 k k k 个节点中,顶点 i i i 到顶点 j j j 的最短路径长度;
转移
在计算 ( i , j ) (i, j) (i,j) 之间的最短路径时,目前 ( i , j ) (i, j) (i,j) 之间的最短路径长度为 d p [ i ] [ j ] dp[i][j] dp[i][j] (此时 i i i 和 j j j 不一定是直接连接) ,发现 i i i 可以通过 k k k 结点到达 j j j ;
得到如下状态转移方程
d p [ k ] [ i ] [ j ] = m i n { 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]}
由于第一位 k k k 只用了 k k k 与 k − 1 k - 1 k−1 所以 dp 数组可以进行滚动数组优化,优化掉 k k k 维,则状态转移方程为
d p [ i ] [ j ] = m i n { 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]}
转移时,先枚举 k k k ,在枚举 i , j i, j i,j ;
松弛操作
更新两点的最短路径又称为松弛操作;
在进行松弛操作时,若 ( i , k ) (i, k) (i,k) 与 ( k , j ) (k, j) (k,j) 之间无边,那么 d p [ i ] [ k ] = d p [ k ] [ j ] = I N F dp[i][k] = dp[k][j] = INF dp[i][k]=dp[k][j]=INF 此时若 I N F INF INF 取 0 x 7 f f f f f f 0x7ffffff 0x7ffffff ,那么 d p [ i ] [ k ] + w [ k ] [ j ] dp[i][k] + w[k][j] dp[i][k]+w[k][j] 就会溢出变成负数,此时松弛操作便会出错,准确来说, 0 x 7 f f f f f f f 0x7fffffff 0x7fffffff 不能满足无穷大加一个有穷的数依然是无穷大,而是变成了一个很小的负数;
因此,可以选用 0 x 3 f 3 f 3 f 3 f 0x3f3f3f3f 0x3f3f3f3f , 0 x 3 f 3 f 3 f 3 f 0x3f3f3f3f 0x3f3f3f3f 的十进制是 1061109567 1061109567 1061109567 ,是 1 0 9 10^9 109 级别的,与 0 x 7 f f f f f f f 0x7fffffff 0x7fffffff 一个数量级,而一般场合下的数据都是小于 1 0 9 10^9 109 的,所以它可以作为无穷大使用而不致出现数据大于无穷大的情形;
另一方面,由于一般的数据都不会大于 1 0 9 10^9 109 ,所以当我们把无穷大加上一个数据时,它并不会溢出,事实上 0 x 3 f 3 f 3 f 3 f + 0 x 3 f 3 f 3 f 3 f = 2122219134 0x3f3f3f3f+ 0x3f3f3f3f=2122219134 0x3f3f3f3f+0x3f3f3f3f=2122219134 ,这个数虽然非常大但却没有超过 i n t int int 的表示范围,因此 0 x 3 f 3 f 3 f 3 f 0x3f3f3f3f 0x3f3f3f3f 还满足了无穷大加无穷大还是无穷大的要求;
4. 例子
d p = [ 0 5 ∞ 7 ∞ 0 4 2 3 3 0 2 ∞ ∞ 1 0 ] dp=\left[ \begin{matrix} 0 & 5 & \infty & 7 \\ \infty & 0 & 4 & 2 \\ 3 & 3 & 0 & 2 \\ \infty & \infty & 1 & 0 \end{matrix} \right] dp=⎣ ⎡0∞3∞503∞∞4017220⎦ ⎤
考虑结点 1 作为中间点,
d p [ 2 ] [ 3 ] = 4 < d p [ 2 ] [ 1 ] + d p [ 1 ] [ 3 〕 = ∞ + ∞ dp[2][3] = 4 < dp[2][1] + dp[1][3〕= \infty + \infty dp[2][3]=4<dp[2][1]+dp[1][3〕=∞+∞ ,不更新;
d p [ 2 ] [ 4 ] = 2 < d p [ 2 ] [ 1 ] + d p [ 1 ] [ 4 ] = ∞ + 7 dp[2][4] = 2 < dp[2][1] + dp[1][4] = \infty + 7 dp[2][4]=2<dp[2][1]+dp[1][4]=∞+7 ,不更新;
d p [ 3 ] [ 2 ] = 3 < d p [ 3 ] [ 1 ] + d p [ 1 ] [ 2 ] = 3 + 5 dp[3][2] = 3 < dp[3][1] + dp[1][2] = 3 + 5 dp[3][2]=3<dp[3][1]+dp[1][2]=3+5 ,不更新;
d p [ 3 ] [ 4 ] = 2 < d p [ 3 ] [ 1 ] + d p [ 1 ] [ 4 ] = 3 + ∞ dp[3][4] = 2 < dp[3][1] + dp[1][4] = 3 + \infty dp[3][4]=2<dp[3][1]+dp[1][4]=3+∞ ,不更新;
d p [ 4 ] [ 2 ] = ∞ = d p [ 4 ] [ 1 ] + d p [ 1 ] [ 2 ] = ∞ + 5 dp[4][2] = \infty = dp[4][1] + dp[1][2] = \infty + 5 dp[4][2]=∞=dp[4][1]+dp[1][2]=∞+5 ,不更新;
d p [ 4 ] [ 3 ] = 1 < d p [ 4 ] [ 1 ] + d p [ 1 ] [ 3 ] = ∞ + ∞ dp[4][3] = 1 < dp[4][1] + dp[1][3] = \infty + \infty dp[4][3]=1<dp[4][1]+dp[1][3]=∞+∞ ,不更新;
没有变化;
最终 d p [ i ] [ j ] dp[i][j] dp[i][j] 存储的就是从 i i i 到 j j j 的最短路径长度。
5. 代码
for (int k = 1; k <= n; k++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j]);
}
}
}
6. 求最短路径方案
思路
Floyd 算法在进行松弛操作时,若松弛操作成功,只能够确定 i i i 到 j j j 是经过 k k k ,不能确定 i i i 的下一个结点是谁,但可知 i i i 到 k k k 的最短路径中 i i i 的下一个结点与 i i i 到 j j j 的最短路径中 i i i 的下一个结点相同;
则使用
p
a
t
h
[
i
]
[
j
]
path[i][j]
path[i][j] 记录
i
i
i 到
j
j
j 最短路径中
i
i
i 的直接后继编号,若松弛成功,则使得 path[i][j] = path[i][k]
,即
i
i
i 到
k
k
k 的最短路径中
i
i
i 的直接后继成为了
i
i
i 到
j
j
j 最短路径中
i
i
i 的直接后继;
若要求字典序最小的一组方案,则在松弛操作时两方案相等时相等时,若当前 path[i][j] > path[i][k]
,则令 path[i][j] = path[i][k]
;
则
p
a
t
h
path
path 的初始化为,对于任意一边
(
x
,
y
)
(x, y)
(x,y) ,有 path[x][y] = y
;
输出时,使用递归进行遍历,传入当前需输出的变量
x
x
x ,输出后,继续递归输出 path[x][t]
,出口即为 path[x][t] == 0
时;
代码如下,
void floyd() {
for (int k = 1; k <= n; k++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (dp[i][j] > dp[i][k] + dp[k][j]) {
dp[i][j] = dp[i][k] + dp[k][j];
path[i][j] = path[i][k];
} else if (dp[i][j] == dp[i][k] + dp[k][j] && path[i][j] > path[i][k]) { // 字典序最小
path[i][j] = path[i][k];
}
}
}
}
}
void print(int x) {
if (path[x][t] == 0) return;
printf("%d ", x);
print(path[x][t]);
}
7. 变形
如果是一个没有边权的图,把相连的两点间的距离设为 dp[i][j]=true
,不相连的两点设为 dp[i]][j]=false
,用 Floyd 算法的变形;
void floyd() {
for (int k = 1; k <= n; k++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
dp[i][j] |= (dp[i][k] & dp[k][j]);
}
}
}
return;
}
用这个办法可以判断一张图中的两点是否相连。
三、Dijkstra算法
1. Dijkstra 算法
Dijkstra 算法是单源最路径算法,即计算起点只有一个的情况到其他点的最短路径,其无法处理存在负边权的情况;
Dijkstra 算法基于一种贪心的思想,基本思想是设 G = ( V , E ) G = (V, E) G=(V,E) 是一个带权有向图,把图中顶点集合 V V V 分成两组:
- 己求出最短路径 d d d 的顶点集合,用 S S S 表示,初始时 S S S 中只有一个源点;
- 其余未确定最短路径的顶点集合,用 U U U 表示;
2. 过程
-
初始时, S S S 只包含源点 V V V , V V V 到自己的距离为 0 ;
U U U 包含除 V V V 外的其他顶点,源点到 U U U 中顶点 i i i 的距离为边上的权,若 V V V 与 i i i 有边连接,则距离就是边权,否则距离为 ∞ \infty ∞ ;
-
从 U U U 中选取一个顶点 u u u ,使得顶点 V V V 到顶点 u u u 的距离最小,然后把顶点 u u u 加入 S S S 中;
-
以顶点 u u u 为新考虑的中间点,对顶点 V V V 到 u u u 中各顶点进行松弛操作;
-
重复步骤 2 和 3 直到 S S S 包含所有的顶点;
3. 例子
以下图为例,以结点 1 作为源点,计算结点1到其他各结点的最短路径;
d i s dis dis 数组存放源点1到结点 i i i 的距离;
t r u e true true 表示已确定的最短路径, f a l s e false false 表示未确定的最短路径;
第1轮松弛操作
dis
① | ② | ③ | ④ |
---|---|---|---|
0 | 5 | ∞ \infty ∞ | 7 |
true | false | false | false |
在 U U U 集合中选择与 v v v (即结点1)距离最短的点结点2,将结点2加入到集合 S S S 中,确定了结点1到结点2的最短路长度为5;
并以结点2作为中间点对源点 v v v 到 U U U 集合中的所有结点进行松弛操作;
d i s [ 3 ] = ∞ < d i s [ 2 ] + w [ 2 ] [ 3 ] = 5 + 4 = 9 dis[3] = \infty < dis[2] + w[2][3] = 5 + 4 = 9 dis[3]=∞<dis[2]+w[2][3]=5+4=9,更新 d i s [ 3 ] = 9 dis[3] = 9 dis[3]=9 ;
d i s [ 4 ] = 7 = d i s [ 2 ] + w [ 2 ] [ 4 ] = 5 + 2 = 7 dis[4] = 7 = dis[2] + w[2][4] = 5 + 2 = 7 dis[4]=7=dis[2]+w[2][4]=5+2=7,不更新;
第2轮松弛操作
dis
① | ② | ③ | ④ |
---|---|---|---|
0 | 5 | 9 | 7 |
true | true | false | false |
在 U U U 集合中选择与 v v v (即结点1)距离最短的点结点4,将结点4加入到集合 S S S 中,确定了结点1到结4点的最短路长度为7;
并以结点4作为中间点对源点 v v v 到 U U U 集合中的所有结点进行松弛操作:
d i s [ 3 ] = ∞ < d i s [ 4 ] + w [ 4 ] [ 3 ] = 7 + 1 = 8 dis[3] = \infty < dis[4] + w[4][3] = 7 + 1 = 8 dis[3]=∞<dis[4]+w[4][3]=7+1=8,更新 d i s [ 3 ] = 8 dis[3] = 8 dis[3]=8 ;
d i s [ 4 ] = 7 = d i s [ 2 ] + w [ 2 ] [ 4 ] = 5 + 2 = 7 dis[4] = 7 = dis[2] + w[2][4] = 5 + 2 = 7 dis[4]=7=dis[2]+w[2][4]=5+2=7,不更新;
第3轮松弛操作
dis
① | ② | ③ | ④ |
---|---|---|---|
0 | 5 | 8 | 7 |
true | true | false | true |
在 U U U 集合中选择与 v v v (即结点1)距离最短的点结点3,将结点3加入到集合 S S S 中,确定了结点1到结3点的最短路长度为8;
U U U 集合为空, D i j k s t r a Dijkstra Dijkstra 算法完成;
dis
① | ② | ③ | ④ |
---|---|---|---|
0 | 5 | 8 | 7 |
true | true | true | true |
4. 证明
Dijkstra 算法也是一种贪心算法。证明 Dijkstra 算法可以找到图中从源点 v v v 到其他所有顶点的最短路径长度;
数学归纳法证明
- 如果顶点 i i i 在 S S S 中,则 d i s [ i ] dis[i] dis[i] 给出了从源点到顶点 i i i 的最短路径长度;
- 如果顶点 i i i 不在 S S S 中,则 d i s [ i ] dis[i] dis[i] 给出了从源点到顶点 i i i的最短特殊路径长度(不一定是最短路径),即该路径上的所有中间顶点都属于 S S S ;
初始时 S S S 中只有一个源点 v v v ,到其他顶点的路径就是从源点到相应顶点的边,显然 1, 2 是成立的;
假设向 S S S 中添加一个新顶点 u u u 之前,条件 1, 2 都成立;
条件1的归纳步骤;
对于每个在添加之前己经存在于 S S S 中的顶点 u u u ,不会有任何变化,条件1依然成立;
在顶点 u u u 加入到 S S S 之前,由假设可知 d i s [ u ] dis[u] dis[u] 是源点到 u u u 的最短路径长度,还要验证从源点 v v v 到顶点 u u u 的最短路径没有经过任何不在 S S S 中的顶点;
假设存在这种情况,即沿着从源点 v v v 到顶点 u u u 的最短路径前进时,会遇到一个或多个不属于 S S S 的顶点不含顶点 u u u 自己),设 x x x 是第一个这样的顶点,如下图所示;
从源点 v v v 到 x x x 是一条特殊路径,距离为 d i s [ x ] dis[x] dis[x] ;
假设 x x x 到 u u u 的距离为 w [ x ] [ u ] w[x][u] w[x][u] ,由于边权非负,即 w [ x ] [ u ] ≥ 0 w[x][u] \geq 0 w[x][u]≥0 ,推出经 x x x 到 u u u 的距离 d i s [ x ] + w [ x ] [ u ] ≥ d i s [ x ] dis[x] + w[x][u] \geq dis[x] dis[x]+w[x][u]≥dis[x] ;
因为算法在选择 x x x 之前先选择了 u u u ,因此 d i s [ x ] ≥ d i s [ u ] dis[x] \geq dis[u] dis[x]≥dis[u] ,这样经过 x x x 到 u u u 的距离 d i s [ x ] + w [ x ] [ u ] ≥ d i s [ x ] ≥ d i s [ u ] dis[x] + w[x][u] \geq dis[x] \geq dis[u] dis[x]+w[x][u]≥dis[x]≥dis[u],至少是 d i s [ u ] dis[u] dis[u] ;
现在验证了当 u u u 加到 S S S 中时, d i s [ u ] dis[u] dis[u] 确定给出源点 v v v 到顶点 u u u 的最短路径长度,条件1是成立的;
条件2的归纳步骤;
考虑不属于 S S S 且不同于 u u u 的一个顶点 w w w ,当 u u u 加到 S S S 中时,从源点 v v v 到 w w w 的最特殊路径有两种可能;
- 不会变化;
- 现在经过顶点 u u u(也可能经过 S S S 中的其他顶点);
对于第2种情况,设 x x x 是到达 w w w 之前经过 S S S 的最后一个,因此这条路径的长度就是 d i s t [ x ] + w [ x ] [ w ] dist[x] + w[x][w] dist[x]+w[x][w] ;
对于任意 S S S 中的顶点 q q q (包括 u u u ),要计算 d i s t [ w ] dist[w] dist[w] 的值,就必须比较 d i s t [ w ] dist[w] dist[w] 原先的值和 d i s t [ q ] + d i s t [ q ] + w [ q ] [ w ] dist[q]+dist[q] + w[q][w] dist[q]+dist[q]+w[q][w] 的大小;
因为算法明确地进行这种比较以计算新的 d i s t [ w ] dist[w] dist[w] 值,所以往 S S S 中加入新顶点 u u u 时, d i s t [ w ] dist[w] dist[w] 为源点 v v v 到顶点 w w w 的最短特殊路径的长度,因此条件2也是成立的;
5. 代码
void dijkstra(int s) {
for (int i = 1; i <= n; i++) {
g[i][i] = 0, dis[i] = g[s][i];
}
vis[s] = true;//s为起点
for (int i = 1; i < n; i++) {
int minn = 2147483647, tot = -1;
for (int j = 1; j <= n; j++) { // 在没有确认最短路的结点集合中找一个顶点tot,使得dis[tot]最小
if (vis[j] == false && dis[j] < minn) {
minn = dis[j], tot = j;
}
}
vis[tot] = true; // tot标记为已确定的最短路径
for (int j = 1; j <= n; j++) { // 枚举与tot相连的每个未确定的最短路的顶点
if (vis[j] == false && dis[tot] + g[tot][j] < dis[j]) {
dis[j] = dis[tot] + g[tot][j]; // 更新最短路径
}
}
}
return;
}
6. 优化
思路
计算 U U U 集合距离起点最小值时,可使用优先对列维护;
代码
void dijkstra(int s) {
memset(dis, 0x3f, sizeof(dis));
dis[s] = 0;
priority_queue < edge > q; // 存储未确定最短路的结点集合
q.push(edge({s, 0}));
while (!q.empty()) {
int t = q.top().to; // 取出距离源点最近的结点编号
q.pop();
if (vis[t]) continue; // 某一个结点可能在松弛操作时多次放入优先队列
vis[t] = true;
for (int i = 0; i < g[t].size(); i++) { // 遍历tot的直接后继且未确定最短路的特点,进行松弛操作
int v = g[t][i].to, tot = g[t][i].tot;
if (dis[v] > dis[t] + tot) { // 如果某结点已经确定最短路,则不会被更新
dis[v] = dis[t] + tot;
q.push(edge({v, dis[v]}));
}
}
}
return;
}
7. 输出方案
则在每一次松弛操作成功时,记录可使此节点松弛操作成功的节点,最后递归输出;
void dijkstra(int s) {
memset(dis, 0x3f, sizeof(dis));
dis[s] = 0;
priority_queue < edge > q; // 存储未确定最短路的结点集合
q.push(edge({s, 0}));
while (!q.empty()) {
int t = q.top().to; // 取出距离源点最近的结点编号
q.pop();
if (vis[t]) continue; // 某一个结点可能在松弛操作时多次放入优先队列
vis[t] = true;
for (int i = 0; i < g[t].size(); i++) { // 遍历tot的直接后继且未确定最短路的特点,进行松弛操作
int v = g[t][i].to, tot = g[t][i].tot;
if (dis[v] > dis[t] + tot) { // 如果某结点已经确定最短路,则不会被更新
dis[v] = dis[t] + tot;
pre[v] = t;
q.push(edge({v, dis[v]}));
} else if (dis[v] == dis[t] + tot && t < pre[v]) { // 字典序最小
pre[v] = t;
}
}
}
return;
}
8. 次短路
问题
对于一张有向图,求 s s s 到 t t t 的严格次短路;
分析
则维护 Dijkstra 的优先队列时,分别维护最短与次短路,当最短路成功进行松弛操作时,次短路的值即为原最短路的值,当最短路松弛操作未成功时但次短路松弛操作成功时,则只更新次短路即可;
代码
#include <cstdio>
#include <queue>
#include <vector>
#include <cstring>
#include <algorithm>
#define MAXN 5005
using namespace std;
int n, m, dis[MAXN][5];
bool vis[MAXN][5];
struct edge {
int to, tot;
};
vector <edge> g[MAXN];
struct node {
int to, tot, p;
bool operator < (const node a) const {
return tot > a.tot;
}
};
void dijkstra(int s) {
memset(dis, 0x3f, sizeof(dis));
dis[s][1] = 0;
priority_queue <node> q;
q.push(node({s, 0, 1}));
while (!q.empty()) {
node t = q.top();
q.pop();
if (vis[t.to][t.p]) continue;
vis[t.to][t.p] = true;
for (int i = 0; i < g[t.to].size(); i++) {
int v = g[t.to][i].to, tot = g[t.to][i].tot;
if (dis[v][1] > dis[t.to][t.p] + tot) {
dis[v][2] = dis[v][1];
dis[v][1] = dis[t.to][t.p] + tot;
q.push(node({v, dis[v][1], 1}));
q.push(node({v, dis[v][2], 2}));
} else if (dis[v][1] < dis[t.to][t.p] + tot && dis[v][2] > dis[t.to][t.p] + tot) {
dis[v][2] = dis[t.to][t.p] + tot;
q.push(node({v, dis[v][2], 2}));
}
}
}
return;
}
int main() {
scanf("%d %d", &n, &m);
for (int i = 1; i <= m; i++) {
int x, y, z;
scanf("%d %d %d", &x, &y, &z);
g[x].push_back(edge({y, z}));
g[y].push_back(edge({x, z}));
}
dijkstra(1);
printf("%d\n", dis[n][2]);
return 0;
}
四、Bellman-Ford 算法
1. Bellman-Ford 算法
Bellman-Ford 算法适用于计算单源最短路径,其最大特点是可以处理存在负边权的情况,但无法处理存在负权回路的情况;
时间复杂度, O ( V ∗ E ) O(V*E) O(V∗E) ,其中, V V V 是顶点数, E E E 是边数;
2. 过程
对从源点到达每个结点的最短路径,每一轮都使用图中所有的边对其进行松弛操作,一共要进行 V − 1 V-1 V−1 轮松弛操作,其中 V V V 为顶点数;
使用 d i s [ i ] dis[i] dis[i] 表示源点到结点 i i i 的最短路径, 进行第 k k k 轮松弛操作后的距离计作 d i s k [ i ] dis_k[i] disk[i] ;
在进行第 k k k 轮松弛操作前,源点 v v v 到达结点 u u u 的距离为 d i s k − 1 [ u ] dis_{k - 1}[u] disk−1[u] .在进行第 k k k 轮松弛操作时, u u u 的所有直接前驱与 u u u 连接的边才有可能对 v v v 到 u u u 的最短路径产生影响;
如下图所示;
则第 k k k 轮松弛操作后, d i s k [ u ] dis_k[u] disk[u] 应为四条路径中的最小值,即,
d i s k [ u ] = min { d i s k − 1 [ u ] d i s k − 1 [ i ] + w [ i ] [ u ] d i s k − 1 [ j ] + w [ j ] [ u ] d i s k − 1 [ k ] + w [ k ] [ u ] dis_k[u] = \min \begin{cases} dis_{k - 1}[u] \\ dis_{k - 1}[i] + w[i][u] \\ dis_{k - 1}[j] + w[j][u] \\ dis_{k - 1}[k] + w[k][u] \\ \end{cases} disk[u]=min⎩ ⎨ ⎧disk−1[u]disk−1[i]+w[i][u]disk−1[j]+w[j][u]disk−1[k]+w[k][u]
即在每一轮松弛操作时,对每一条边所到达的结点 i i i ( i i i 为 u u u 的直接前驱)都要进行松弛操作;
d i s k [ u ] = min { d i s k − 1 [ u ] , min 1 ≤ i ≤ n , i ≠ u { d i s k − 1 [ i ] + w [ i ] [ u ] } } dis_k[u] = \min \{ dis_{k - 1}[u], \min_{1 \leq i \leq n, i \neq u}\{dis_{k - 1}[i] + w[i][u]\}\} disk[u]=min{disk−1[u],1≤i≤n,i=umin{disk−1[i]+w[i][u]}}
3. 例子
如下图,以结点1作为起点演示 Bellman-Ford 算法;
dis数组初始值
0 | ∞ \infty ∞ | ∞ \infty ∞ | ∞ \infty ∞ | ∞ \infty ∞ | ∞ \infty ∞ | ∞ \infty ∞ |
---|
第一轮松弛操作
考虑结点 2, 3, 4 的前驱结点;
d
i
s
1
[
2
]
=
∞
>
d
i
s
0
[
1
]
+
w
[
1
]
[
2
]
=
0
+
4
=
4
dis_1[2] = \infty > dis_0[1] + w[1][2] = 0 + 4 = 4
dis1[2]=∞>dis0[1]+w[1][2]=0+4=4 , 更新
d
i
s
1
[
2
]
=
4
dis_1[2] = 4
dis1[2]=4 ;
d
i
s
1
[
3
]
=
∞
>
d
i
s
0
[
1
]
+
w
[
1
]
[
3
]
=
0
+
(
−
6
)
=
−
6
dis_1[3] = \infty > dis_0[1] + w[1][3] = 0 + (-6) = -6
dis1[3]=∞>dis0[1]+w[1][3]=0+(−6)=−6 , 更新
d
i
s
1
[
3
]
=
−
6
dis_1[3] = -6
dis1[3]=−6 ;
d
i
s
1
[
4
]
=
∞
>
d
i
s
0
[
1
]
+
w
[
1
]
[
4
]
=
0
+
6
=
6
dis_1[4] = \infty > dis_0[1] + w[1][4] = 0 + 6 = 6
dis1[4]=∞>dis0[1]+w[1][4]=0+6=6 , 更新
d
i
s
1
[
4
]
=
6
dis_1[4] = 6
dis1[4]=6 ;
dis数组
0 | 4 | -6 | 6 | ∞ \infty ∞ | ∞ \infty ∞ | ∞ \infty ∞ |
---|
考虑结点 5 的前驱结点;
d
i
s
1
[
5
]
=
∞
>
d
i
s
0
[
2
]
+
w
[
2
]
[
5
]
=
4
+
7
=
11
dis_1[5] = \infty > dis_0[2] + w[2][5] = 4 + 7 = 11
dis1[5]=∞>dis0[2]+w[2][5]=4+7=11 , 更新
d
i
s
1
[
5
]
=
11
dis_1[5] = 11
dis1[5]=11 ;
d
i
s
1
[
5
]
=
11
>
d
i
s
0
[
3
]
+
w
[
3
]
[
5
]
=
−
6
+
6
=
0
dis_1[5] = 11 > dis_0[3] + w[3][5] = -6 + 6 = 0
dis1[5]=11>dis0[3]+w[3][5]=−6+6=0 , 更新
d
i
s
1
[
5
]
=
0
dis_1[5] = 0
dis1[5]=0 ;
d
i
s
1
[
5
]
=
0
<
d
i
s
0
[
6
]
+
w
[
6
]
[
5
]
=
∞
+
1
=
∞
dis_1[5] = 0 < dis_0[6] + w[6][5] = \infty + 1 = \infty
dis1[5]=0<dis0[6]+w[6][5]=∞+1=∞ , 不更新;
dis数组
0 | 4 | -6 | 6 | 0 | ∞ \infty ∞ | ∞ \infty ∞ |
---|
考虑结点6的前驱结点;
d
i
s
1
[
6
]
=
∞
>
d
i
s
0
[
3
]
+
w
[
3
]
[
6
]
=
−
6
+
4
=
−
2
dis_1[6] = \infty > dis_0[3] + w[3][6] = -6 + 4 = -2
dis1[6]=∞>dis0[3]+w[3][6]=−6+4=−2 , 更新
d
i
s
1
[
6
]
=
−
2
dis_1[6] = -2
dis1[6]=−2 ;
d
i
s
1
[
6
]
=
−
2
<
d
i
s
0
[
4
]
+
w
[
4
]
[
6
]
=
6
+
5
=
11
dis_1[6] = -2 < dis_0[4] + w[4][6] = 6 + 5 = 11
dis1[6]=−2<dis0[4]+w[4][6]=6+5=11 , 不更新 ;
dis数组
0 | 4 | -6 | 6 | 0 | -2 | ∞ \infty ∞ |
---|
考虑结点7的前驱结点;
d
i
s
1
[
7
]
=
∞
>
d
i
s
0
[
5
]
+
w
[
5
]
[
7
]
=
0
+
6
=
6
dis_1[7] = \infty > dis_0[5] + w[5][7] = 0 + 6 = 6
dis1[7]=∞>dis0[5]+w[5][7]=0+6=6,更新
d
i
s
1
[
7
]
=
6
dis_1[7] = 6
dis1[7]=6;
d
i
s
1
[
7
]
=
6
>
d
i
s
0
[
6
]
+
w
[
6
]
[
7
]
=
−
2
+
(
−
8
)
=
−
10
dis_1[7] = 6 > dis_0[6] + w[6][7] = -2 + (-8) = -10
dis1[7]=6>dis0[6]+w[6][7]=−2+(−8)=−10,更新
d
i
s
1
[
7
]
=
−
10
dis_1[7] = -10
dis1[7]=−10;
dis数组:
0 | 4 | -6 | 6 | 0 | -2 | -10 |
---|
第一轮松弛操作结束
dis数组操作如下
d i s k [ 1 ] dis_k[1] disk[1] | d i s k [ 2 ] dis_k[2] disk[2] | d i s k [ 3 ] dis_k[3] disk[3] | d i s k [ 4 ] dis_k[4] disk[4] | d i s k [ 5 ] dis_k[5] disk[5] | d i s k [ 6 ] dis_k[6] disk[6] | d i s k [ 7 ] dis_k[7] disk[7] | |
---|---|---|---|---|---|---|---|
第一轮 | 0 | 4 | -6 | 6 | 0 | -2 | -10 |
第二轮 | 0 | 4 | -6 | 6 | -1 | -2 | -10 |
第三轮 | 0 | 4 | -6 | 6 | -1 | -2 | -10 |
第四轮 | 0 | 4 | -6 | 6 | -1 | -2 | -10 |
第五轮 | 0 | 4 | -6 | 6 | -1 | -2 | -10 |
第六轮 | 0 | 4 | -6 | 6 | -1 | -2 | -10 |
4. 解释
通过例子发现,从第二轮松弛操作之后,后面的最短路径值都没有发生过改变, Bellman-Ford 算法可以在这里进行优化。若某一轮松弛操作没有任何值发生变化,则算法可以直接结束;
每次松弛操作实际上是对相邻结点的访问,第 k k k 轮松弛操作保证了所有经过 k k k 条边的最短路径最短;
由于图的最短路径最长不会经过超过 V − 1 V-1 V−1 条边,所以可知 Bellman-Ford 算法所得为最短路径。
5. 判断负环
在执行完 V − 1 V-1 V−1 轮松弛操作之后,若发现还能够成功松弛操作,则说明图中存在负环;否则不存在负回路;
图为负环,需进行2轮松弛操作;
第一轮松弛操作
d
i
s
1
[
2
]
=
d
i
s
0
[
1
]
+
w
[
1
]
[
2
]
=
0
+
1
=
1
dis_1[2] = dis_0[1] + w[1][2] = 0 + 1 = 1
dis1[2]=dis0[1]+w[1][2]=0+1=1 ;
d
i
s
1
[
3
]
=
d
i
s
0
[
2
]
+
w
[
2
]
[
3
]
=
1
+
(
−
4
)
=
−
3
dis_1[3] = dis_0[2] + w[2][3] = 1 + (-4) = -3
dis1[3]=dis0[2]+w[2][3]=1+(−4)=−3 ;
d
i
s
1
[
l
]
=
d
i
s
0
[
3
]
+
w
[
3
]
[
1
]
=
−
3
+
2
=
−
1
dis_1[l] = dis_0[3] + w[3][1] = -3 + 2 = -1
dis1[l]=dis0[3]+w[3][1]=−3+2=−1 ;
第二轮松弛操作
d
i
s
2
[
2
]
=
d
i
s
1
[
1
]
+
w
[
1
]
[
2
]
=
−
1
+
1
=
0
dis_2[2] = dis_1[1] + w[1][2] = -1 + 1 = 0
dis2[2]=dis1[1]+w[1][2]=−1+1=0 ;
d
i
s
2
[
3
]
=
d
i
s
1
[
2
]
+
w
[
2
]
[
3
]
=
−
3
dis_2[3] = dis_1[2] + w[2][3] = -3
dis2[3]=dis1[2]+w[2][3]=−3 ;
d
i
s
2
[
1
]
=
d
i
s
1
[
3
]
+
w
[
3
]
[
1
]
=
−
3
+
(
−
4
)
=
−
7
dis_2[1] = dis_1[3] + w[3][1] = -3 + (-4) = -7
dis2[1]=dis1[3]+w[3][1]=−3+(−4)=−7 ;
若第三轮松弛操作仍能成功,则说明存在负环;
如果存在从源点可达的负权值回路(负回路),则最短路径不存在,因为可以重复走这个回路,使得路径无穷小。
6. 代码
bool Bellman_Ford (int s) { // s为起点
memset(dis, 0x3f, sizeof(dis));
dis[s] = 0;
for (int i = 1; i < n; i++) { // n个顶点
for (int j = 1; j <= m; j++) { // m条边
dis[g[j].to] = min(dis[g[j].to], dis[g[j].from] + g[j].tot);
}
}
for (int j = 1; j <= m; j++) {
if (dis[g[j].to] > dis[g[j].from] + g[j].tot) {
return false; // 有负环
}
}
return true; // 没有负环
}
五、SPFA 算法
1. SPFA 算法
SPFA 算法也是一个求单源最短路径的算法,全称是 Shortest Path Faster Algorithm(SPFA) ,是由西南交通大学段凡丁老师1994年发明的;
当给定的图存在负权边时,Dijkstra 算法不再适合,而 Bellman-Ford 算法的时间复杂度又过高,此时可以采用 SPFA 算法;
但 SPFA 算法仍然不适合含负权回路的图;
2. 过程
初始时将起点加入队列;
每次从队列中取出一个元素,并对所有与它相邻的点进行修改,若某个相邻的点修改成功,则将其入队,直到队列为空时算法结束;
这个算法,简单的说就是队列优化的 Bellman-Ford,利用了每个点不会更新次数太多的特点发明的此算法;
SPFA 在形式上和广度优先搜索非常类似,不同的是广度优先搜索中一个点出了队列就不可能重新进入队列,但是 SPFA 中一个点可能在出队列之后再次被放入队列,也就是说一个点修改过其它的点之后,过了一段时间可能会获得更短的路径,于是再次用来修改其它的点,这样反复进行下去;
对于负环时,则负环上的节点会一直进行松弛操作,即一直进队,则判断当有节点 n n n 次进入队时,则有负环;
3. 代码
bool SPFA(int s) {
memset(dis, 0x3f, sizeof(dis));
dis[s] = 0;
queue <int> q;
q.push(s); // 将起点入队
while (!q.empty()) {
int t = q.front();
q.pop();
vis[t] = false; // 出队首元素,并且将元素标记为出队
for (int i = 0; i < g[t].size(); i++) { // 遍历结点u的所有后继结点,进行松弛操作
int v = g[t][i].to, tot = g[t][i].tot;
if (dis[v] > dis[t] + tot) {
dis[v] = dis[t] + tot;
if (!vis[v]) { // 若之前以入队,则不需要重复入队
vis[v] = true;
tot1[v]++; // 入队次数 = n, 有负环
if (tot1[v] == n) return false;
q.push(v);
}
}
}
}
return true;
}
4. 第K短路
题目
给定一张N个点(编号1,2…N),M条边的有向图,求从起点S到终点T的第K短路的长度,路径允许重复经过点或边;
分析
求K短路可用类似 S P F A SPFA SPFA 算法的方法,即当第K次搜索到终点时,就为第K短路;
由于题目给出了起点与终点,可考虑用A*算法优化;
估价函数,
根据估价函数的设计准则,当前点 x x x 到 e e e 点的估计距离应不大于 x x x 到 e e e 点的实际距离,则把估价函数定为从 x x x 到 e e e 的最短路长度;
则程序思路如下
-
输入时,正反向建两个图,用反向建的图处理结点 x x x 到终点 e e e 点的最短路;
-
从起点开始A*搜索扩展状态,当第K次搜索到 e e e 结点时,就得到路径长;
当起点为终点时,会把起点算作一次,所以次数+1;
代码
#include <cstdio>
#include <queue>
#include <vector>
#include <cstring>
#include <algorithm>
#define MAXN 1005
#define INF 0x3f3f3f3f
using namespace std;
int n, m, s, e, k;
struct edge {
int to, tot;
};
vector < edge > g1[MAXN], g2[MAXN];
int dis[MAXN];
bool vis[MAXN];
void SPFA (int s) {
memset(dis, INF, sizeof(dis));
dis[s] = 0;
queue < int > q;
q.push(s);
while (!q.empty()) {
int tot = q.front();
q.pop();
vis[tot] = false;
for (int i = 0; i < g2[tot].size(); i++) {
int v = g2[tot][i].to, z = g2[tot][i].tot;
if (dis[v] > dis[tot] + z) {
dis[v] = dis[tot] + z;
if (!vis[v]) {
vis[v] = true;
q.push(v);
}
}
}
}
}
struct node {
int to, z, tot;
bool operator < (const node t) const {
return t.tot < tot;
}
};
int a_star(int s, int e, int k) {
if (dis[s] == INF) return -1;
if (s == e) k++;
int tot = 0;
priority_queue < node > q;
node t;
q.push( node ( { s, 0, 0 + vis[s] } ) );
while (!q.empty()) {
t = q.top();
q.pop();
if (t.to == e) tot++;
if (tot == k) return t.z;
for (int i = 0; i < g1[t.to].size(); i++) {
q.push( node ( { g1[t.to][i].to, t.z + g1[t.to][i].tot, t.z + g1[t.to][i].tot + dis[g1[t.to][i].to] } ) );
}
}
return -1;
}
int main() {
scanf("%d %d", &n, &m);
for (int i = 1; i <= m; i++) {
int a, b, l;
scanf("%d %d %d", &a, &b, &l);
g1[a].push_back( edge ( { b, l } ) );
g2[b].push_back( edge ( { a, l } ) );
}
scanf("%d %d %d", &s, &e, &k);
SPFA(e);
printf("%d", a_star(s, e, k));
return 0;
}
六、比较
算法 | 用途 | 时间复杂度 | 特点 |
---|---|---|---|
Dijkstra | 单源最短路径 | O ( n 2 ) O(n^2) O(n2) | 不适合负权及负权回路 |
SPFA | 单源最短路径 | O ( e ) O(e) O(e) | 不适合负权回路 |
Bellman-Ford | 单源最短路径 | O ( n e ) O(ne) O(ne) | 不适合负权回路 |
Floyd | 多源最短路径 | O ( n 3 ) O(n^3) O(n3) | 不适合负权回路 |