图的最短路
一、定义
从图中某一顶点 (源点) 出发,到达另一顶点 (终点) 的所有路径中 (路径可能不存在或者存在不止一条) , 各边权值之和最小的路径,称为最短路径;
最短路径问题是图论的经典问题;
最短路径问题分为两类,
- 求单个顶点和其他所有顶点的最短路径,称为单源最短路径问题 ;
- 求所有顶点相互之间的最短路径,称为多源最短路径问题 ;
对于以上两类最短路径问题,都有相应的有效算法予以解决。
二、Floyd 算法
1. Floyd 算法
Floyd 算法又称为插点法,是一种用于寻找给定的加权图中多源点之间最短路径的算法;
2. 特点
Floyd 算法用于计算多元最短路,可计算出现负边权时的最短路,实际应用中,很多题目不是求如何用 Floyd 求最短路,而是用 Floyd 的动态规划思想来解决类似 Floyd 的问题;
3. 实现
Floyd 算法基于动态规划方法,使用 dp 存放当前顶点之间的最短路径长度;
状态
dp[k][i][j]dp[k][i][j]dp[k][i][j] 为前 kkk 个节点中,顶点 iii 到顶点 jjj 的最短路径长度;
转移
在计算 (i,j)(i, j)(i,j) 之间的最短路径时,目前 (i,j)(i, j)(i,j) 之间的最短路径长度为 dp[i][j]dp[i][j]dp[i][j] (此时 iii 和 jjj 不一定是直接连接) ,发现 iii 可以通过 kkk 结点到达 jjj ;
得到如下状态转移方程
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]\} dp[k][i][j]=min{dp[k−1][i][j],dp[k−1][i][k]+dp[k−1][k][j]}
由于第一位 kkk 只用了 kkk 与 k−1k - 1k−1 所以 dp 数组可以进行滚动数组优化,优化掉 kkk 维,则状态转移方程为
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]\} dp[i][j]=min{dp[i][j],dp[i][k]+dp[k][j]}
转移时,先枚举 kkk ,在枚举 i,ji, ji,j ;
松弛操作
更新两点的最短路径又称为松弛操作;
在进行松弛操作时,若 (i,k)(i, k)(i,k) 与 (k,j)(k, j)(k,j) 之间无边,那么 dp[i][k]=dp[k][j]=INFdp[i][k] = dp[k][j] = INFdp[i][k]=dp[k][j]=INF 此时若 INFINFINF 取 0x7ffffff0x7ffffff0x7ffffff ,那么 dp[i][k]+w[k][j]dp[i][k] + w[k][j]dp[i][k]+w[k][j] 就会溢出变成负数,此时松弛操作便会出错,准确来说, 0x7fffffff0x7fffffff0x7fffffff 不能满足无穷大加一个有穷的数依然是无穷大,而是变成了一个很小的负数;
因此,可以选用 0x3f3f3f3f0x3f3f3f3f0x3f3f3f3f , 0x3f3f3f3f0x3f3f3f3f0x3f3f3f3f 的十进制是 106110956710611095671061109567 ,是 10910^9109 级别的,与 0x7fffffff0x7fffffff0x7fffffff 一个数量级,而一般场合下的数据都是小于 10910^9109 的,所以它可以作为无穷大使用而不致出现数据大于无穷大的情形;
另一方面,由于一般的数据都不会大于 10910^9109 ,所以当我们把无穷大加上一个数据时,它并不会溢出,事实上 0x3f3f3f3f+0x3f3f3f3f=21222191340x3f3f3f3f+ 0x3f3f3f3f=21222191340x3f3f3f3f+0x3f3f3f3f=2122219134 ,这个数虽然非常大但却没有超过 intintint 的表示范围,因此 0x3f3f3f3f0x3f3f3f3f0x3f3f3f3f 还满足了无穷大加无穷大还是无穷大的要求;
4. 例子
dp=[05∞7∞0423302∞∞10] 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 作为中间点,
dp[2][3]=4<dp[2][1]+dp[1][3〕=∞+∞dp[2][3] = 4 < dp[2][1] + dp[1][3〕= \infty + \inftydp[2][3]=4<dp[2][1]+dp[1][3〕=∞+∞ ,不更新;
dp[2][4]=2<dp[2][1]+dp[1][4]=∞+7dp[2][4] = 2 < dp[2][1] + dp[1][4] = \infty + 7dp[2][4]=2<dp[2][1]+dp[1][4]=∞+7 ,不更新;
dp[3][2]=3<dp[3][1]+dp[1][2]=3+5dp[3][2] = 3 < dp[3][1] + dp[1][2] = 3 + 5dp[3][2]=3<dp[3][1]+dp[1][2]=3+5 ,不更新;
dp[3][4]=2<dp[3][1]+dp[1][4]=3+∞dp[3][4] = 2 < dp[3][1] + dp[1][4] = 3 + \inftydp[3][4]=2<dp[3][1]+dp[1][4]=3+∞ ,不更新;
dp[4][2]=∞=dp[4][1]+dp[1][2]=∞+5dp[4][2] = \infty = dp[4][1] + dp[1][2] = \infty + 5dp[4][2]=∞=dp[4][1]+dp[1][2]=∞+5 ,不更新;
dp[4][3]=1<dp[4][1]+dp[1][3]=∞+∞dp[4][3] = 1 < dp[4][1] + dp[1][3] = \infty + \inftydp[4][3]=1<dp[4][1]+dp[1][3]=∞+∞ ,不更新;
没有变化;
最终 dp[i][j]dp[i][j]dp[i][j] 存储的就是从 iii 到 jjj 的最短路径长度。
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 算法在进行松弛操作时,若松弛操作成功,只能够确定 iii 到 jjj 是经过 kkk ,不能确定 iii 的下一个结点是谁,但可知 iii 到 kkk 的最短路径中 iii 的下一个结点与 iii 到 jjj 的最短路径中 iii 的下一个结点相同;
则使用 path[i][j]path[i][j]path[i][j] 记录 iii 到 jjj 最短路径中 iii 的直接后继编号,若松弛成功,则使得 path[i][j] = path[i][k]
,即 iii 到 kkk 的最短路径中 iii 的直接后继成为了 iii 到 jjj 最短路径中 iii 的直接后继;
若要求字典序最小的一组方案,则在松弛操作时两方案相等时相等时,若当前 path[i][j] > path[i][k]
,则令 path[i][j] = path[i][k]
;
则 pathpathpath 的初始化为,对于任意一边 (x,y)(x, y)(x,y) ,有 path[x][y] = y
;
输出时,使用递归进行遍历,传入当前需输出的变量 xxx ,输出后,继续递归输出 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) 是一个带权有向图,把图中顶点集合 VVV 分成两组:
- 己求出最短路径 ddd 的顶点集合,用 SSS 表示,初始时 SSS 中只有一个源点;
- 其余未确定最短路径的顶点集合,用 UUU 表示;
2. 过程
-
初始时, SSS 只包含源点 VVV , VVV 到自己的距离为 0 ;
UUU 包含除 VVV 外的其他顶点,源点到 UUU 中顶点 iii 的距离为边上的权,若 VVV 与 iii 有边连接,则距离就是边权,否则距离为 ∞\infty∞ ;
-
从 UUU 中选取一个顶点 uuu ,使得顶点 VVV 到顶点 uuu 的距离最小,然后把顶点 uuu 加入 SSS 中;
-
以顶点 uuu 为新考虑的中间点,对顶点 VVV 到 uuu 中各顶点进行松弛操作;
-
重复步骤 2 和 3 直到 SSS 包含所有的顶点;
3. 例子
以下图为例,以结点 1 作为源点,计算结点1到其他各结点的最短路径;
disdisdis 数组存放源点1到结点 iii 的距离;
truetruetrue 表示已确定的最短路径, falsefalsefalse 表示未确定的最短路径;
第1轮松弛操作
dis
① | ② | ③ | ④ |
---|---|---|---|
0 | 5 | ∞\infty∞ | 7 |
true | false | false | false |
在 UUU 集合中选择与 vvv (即结点1)距离最短的点结点2,将结点2加入到集合 SSS 中,确定了结点1到结点2的最短路长度为5;
并以结点2作为中间点对源点 vvv 到 UUU 集合中的所有结点进行松弛操作;
dis[3]=∞<dis[2]+w[2][3]=5+4=9dis[3] = \infty < dis[2] + w[2][3] = 5 + 4 = 9dis[3]=∞<dis[2]+w[2][3]=5+4=9,更新 dis[3]=9dis[3] = 9dis[3]=9 ;
dis[4]=7=dis[2]+w[2][4]=5+2=7dis[4] = 7 = dis[2] + w[2][4] = 5 + 2 = 7dis[4]=7=dis[2]+w[2][4]=5+2=7,不更新;
第2轮松弛操作
dis
① | ② | ③ | ④ |
---|---|---|---|
0 | 5 | 9 | 7 |
true | true | false | false |
在 UUU 集合中选择与 vvv (即结点1)距离最短的点结点4,将结点4加入到集合 SSS 中,确定了结点1到结4点的最短路长度为7;
并以结点4作为中间点对源点 vvv 到 UUU 集合中的所有结点进行松弛操作:
dis[3]=∞<dis[4]+w[4][3]=7+1=8dis[3] = \infty < dis[4] + w[4][3] = 7 + 1 = 8dis[3]=∞<dis[4]+w[4][3]=7+1=8,更新 dis[3]=8dis[3] = 8dis[3]=8 ;
dis[4]=7=dis[2]+w[2][4]=5+2=7dis[4] = 7 = dis[2] + w[2][4] = 5 + 2 = 7dis[4]=7=dis[2]+w[2][4]=5+2=7,不更新;
第3轮松弛操作
dis
① | ② | ③ | ④ |
---|---|---|---|
0 | 5 | 8 | 7 |
true | true | false | true |
在 UUU 集合中选择与 vvv (即结点1)距离最短的点结点3,将结点3加入到集合 SSS 中,确定了结点1到结3点的最短路长度为8;
UUU 集合为空, DijkstraDijkstraDijkstra 算法完成;
dis
① | ② | ③ | ④ |
---|---|---|---|
0 | 5 | 8 | 7 |
true | true | true | true |
4. 证明
Dijkstra 算法也是一种贪心算法。证明 Dijkstra 算法可以找到图中从源点 vvv 到其他所有顶点的最短路径长度;
数学归纳法证明
- 如果顶点 iii 在 SSS 中,则 dis[i]dis[i]dis[i] 给出了从源点到顶点 iii 的最短路径长度;
- 如果顶点 iii 不在 SSS 中,则 dis[i]dis[i]dis[i] 给出了从源点到顶点iii的最短特殊路径长度(不一定是最短路径),即该路径上的所有中间顶点都属于 SSS ;
初始时 SSS 中只有一个源点 vvv ,到其他顶点的路径就是从源点到相应顶点的边,显然 1, 2 是成立的;
假设向 SSS 中添加一个新顶点 uuu 之前,条件 1, 2 都成立;
条件1的归纳步骤;
对于每个在添加之前己经存在于 SSS 中的顶点 uuu ,不会有任何变化,条件1依然成立;
在顶点 uuu 加入到 SSS 之前,由假设可知 dis[u]dis[u]dis[u] 是源点到 uuu 的最短路径长度,还要验证从源点 vvv 到顶点 uuu 的最短路径没有经过任何不在 SSS 中的顶点;
假设存在这种情况,即沿着从源点 vvv 到顶点 uuu 的最短路径前进时,会遇到一个或多个不属于SSS 的顶点不含顶点 uuu 自己),设 xxx 是第一个这样的顶点,如下图所示;
从源点 vvv 到 xxx 是一条特殊路径,距离为 dis[x]dis[x]dis[x] ;
假设 xxx 到 uuu 的距离为 w[x][u]w[x][u]w[x][u] ,由于边权非负,即 w[x][u]≥0w[x][u] \geq 0w[x][u]≥0 ,推出经 xxx 到 uuu 的距离 dis[x]+w[x][u]≥dis[x]dis[x] + w[x][u] \geq dis[x]dis[x]+w[x][u]≥dis[x] ;
因为算法在选择 xxx 之前先选择了 uuu ,因此 dis[x]≥dis[u]dis[x] \geq dis[u]dis[x]≥dis[u] ,这样经过 xxx 到 uuu 的距离dis[x]+w[x][u]≥dis[x]≥dis[u]dis[x] + w[x][u] \geq dis[x] \geq dis[u]dis[x]+w[x][u]≥dis[x]≥dis[u],至少是 dis[u]dis[u]dis[u] ;
现在验证了当 uuu 加到 SSS 中时,dis[u]dis[u]dis[u] 确定给出源点 vvv 到顶点 uuu 的最短路径长度,条件1是成立的;
条件2的归纳步骤;
考虑不属于 SSS 且不同于 uuu 的一个顶点 www ,当 uuu 加到 SSS 中时,从源点 vvv 到 www 的最特殊路径有两种可能;
- 不会变化;
- 现在经过顶点 uuu(也可能经过 SSS 中的其他顶点);
对于第2种情况,设 xxx 是到达 www 之前经过 SSS 的最后一个,因此这条路径的长度就是 dist[x]+w[x][w]dist[x] + w[x][w]dist[x]+w[x][w] ;
对于任意 SSS 中的顶点 qqq (包括 uuu ),要计算 dist[w]dist[w]dist[w] 的值,就必须比较dist[w]dist[w]dist[w] 原先的值和 dist[q]+dist[q]+w[q][w]dist[q]+dist[q] + w[q][w]dist[q]+dist[q]+w[q][w] 的大小;
因为算法明确地进行这种比较以计算新的 dist[w]dist[w]dist[w] 值,所以往 SSS 中加入新顶点 uuu 时,dist[w]dist[w]dist[w] 为源点 vvv 到顶点 www 的最短特殊路径的长度,因此条件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. 优化
思路
计算 UUU 集合距离起点最小值时,可使用优先对列维护;
代码
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. 次短路
问题
对于一张有向图,求 sss 到 ttt 的严格次短路;
分析
则维护 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) ,其中, VVV 是顶点数,EEE 是边数;
2. 过程
对从源点到达每个结点的最短路径,每一轮都使用图中所有的边对其进行松弛操作,一共要进行 V−1V-1V−1 轮松弛操作,其中 VVV 为顶点数;
使用 dis[i]dis[i]dis[i] 表示源点到结点 iii 的最短路径, 进行第 kkk 轮松弛操作后的距离计作 disk[i]dis_k[i]disk[i] ;
在进行第 kkk 轮松弛操作前,源点 vvv 到达结点 uuu 的距离为 disk−1[u]dis_{k - 1}[u]disk−1[u] .在进行第 kkk 轮松弛操作时,uuu 的所有直接前驱与 uuu 连接的边才有可能对 vvv 到 uuu 的最短路径产生影响;
如下图所示;
则第 kkk 轮松弛操作后,disk[u]dis_k[u]disk[u] 应为四条路径中的最小值,即,
disk[u]=min{disk−1[u]disk−1[i]+w[i][u]disk−1[j]+w[j][u]disk−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]
即在每一轮松弛操作时,对每一条边所到达的结点 iii ( iii 为 uuu 的直接前驱)都要进行松弛操作;
disk[u]=min{disk−1[u],min1≤i≤n,i≠u{disk−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 的前驱结点;
dis1[2]=∞>dis0[1]+w[1][2]=0+4=4dis_1[2] = \infty > dis_0[1] + w[1][2] = 0 + 4 = 4dis1[2]=∞>dis0[1]+w[1][2]=0+4=4 , 更新 dis1[2]=4dis_1[2] = 4dis1[2]=4 ;
dis1[3]=∞>dis0[1]+w[1][3]=0+(−6)=−6dis_1[3] = \infty > dis_0[1] + w[1][3] = 0 + (-6) = -6dis1[3]=∞>dis0[1]+w[1][3]=0+(−6)=−6 , 更新 dis1[3]=−6dis_1[3] = -6dis1[3]=−6 ;
dis1[4]=∞>dis0[1]+w[1][4]=0+6=6dis_1[4] = \infty > dis_0[1] + w[1][4] = 0 + 6 = 6dis1[4]=∞>dis0[1]+w[1][4]=0+6=6 , 更新 dis1[4]=6dis_1[4] = 6dis1[4]=6 ;
dis数组
0 | 4 | -6 | 6 | ∞\infty∞ | ∞\infty∞ | ∞\infty∞ |
---|
考虑结点 5 的前驱结点;
dis1[5]=∞>dis0[2]+w[2][5]=4+7=11dis_1[5] = \infty > dis_0[2] + w[2][5] = 4 + 7 = 11dis1[5]=∞>dis0[2]+w[2][5]=4+7=11 , 更新 dis1[5]=11dis_1[5] = 11dis1[5]=11 ;
dis1[5]=11>dis0[3]+w[3][5]=−6+6=0dis_1[5] = 11 > dis_0[3] + w[3][5] = -6 + 6 = 0dis1[5]=11>dis0[3]+w[3][5]=−6+6=0 , 更新 dis1[5]=0dis_1[5] = 0dis1[5]=0 ;
dis1[5]=0<dis0[6]+w[6][5]=∞+1=∞dis_1[5] = 0 < dis_0[6] + w[6][5] = \infty + 1 = \inftydis1[5]=0<dis0[6]+w[6][5]=∞+1=∞ , 不更新;
dis数组
0 | 4 | -6 | 6 | 0 | ∞\infty∞ | ∞\infty∞ |
---|
考虑结点6的前驱结点;
dis1[6]=∞>dis0[3]+w[3][6]=−6+4=−2dis_1[6] = \infty > dis_0[3] + w[3][6] = -6 + 4 = -2dis1[6]=∞>dis0[3]+w[3][6]=−6+4=−2 , 更新dis1[6]=−2dis_1[6] = -2dis1[6]=−2 ;
dis1[6]=−2<dis0[4]+w[4][6]=6+5=11dis_1[6] = -2 < dis_0[4] + w[4][6] = 6 + 5 = 11dis1[6]=−2<dis0[4]+w[4][6]=6+5=11 , 不更新 ;
dis数组
0 | 4 | -6 | 6 | 0 | -2 | ∞\infty∞ |
---|
考虑结点7的前驱结点;
dis1[7]=∞>dis0[5]+w[5][7]=0+6=6dis_1[7] = \infty > dis_0[5] + w[5][7] = 0 + 6 = 6dis1[7]=∞>dis0[5]+w[5][7]=0+6=6,更新dis1[7]=6dis_1[7] = 6dis1[7]=6;
dis1[7]=6>dis0[6]+w[6][7]=−2+(−8)=−10dis_1[7] = 6 > dis_0[6] + w[6][7] = -2 + (-8) = -10dis1[7]=6>dis0[6]+w[6][7]=−2+(−8)=−10,更新dis1[7]=−10dis_1[7] = -10dis1[7]=−10;
dis数组:
0 | 4 | -6 | 6 | 0 | -2 | -10 |
---|
第一轮松弛操作结束
dis数组操作如下
disk[1]dis_k[1]disk[1] | disk[2]dis_k[2]disk[2] | disk[3]dis_k[3]disk[3] | disk[4]dis_k[4]disk[4] | disk[5]dis_k[5]disk[5] | disk[6]dis_k[6]disk[6] | disk[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 算法可以在这里进行优化。若某一轮松弛操作没有任何值发生变化,则算法可以直接结束;
每次松弛操作实际上是对相邻结点的访问,第 kkk 轮松弛操作保证了所有经过 kkk 条边的最短路径最短;
由于图的最短路径最长不会经过超过 V−1V-1V−1 条边,所以可知 Bellman-Ford 算法所得为最短路径。
5. 判断负环
在执行完 V−1V-1V−1 轮松弛操作之后,若发现还能够成功松弛操作,则说明图中存在负环;否则不存在负回路;
图为负环,需进行2轮松弛操作;
第一轮松弛操作
dis1[2]=dis0[1]+w[1][2]=0+1=1dis_1[2] = dis_0[1] + w[1][2] = 0 + 1 = 1dis1[2]=dis0[1]+w[1][2]=0+1=1 ;
dis1[3]=dis0[2]+w[2][3]=1+(−4)=−3dis_1[3] = dis_0[2] + w[2][3] = 1 + (-4) = -3dis1[3]=dis0[2]+w[2][3]=1+(−4)=−3 ;
dis1[l]=dis0[3]+w[3][1]=−3+2=−1dis_1[l] = dis_0[3] + w[3][1] = -3 + 2 = -1dis1[l]=dis0[3]+w[3][1]=−3+2=−1 ;
第二轮松弛操作
dis2[2]=dis1[1]+w[1][2]=−1+1=0dis_2[2] = dis_1[1] + w[1][2] = -1 + 1 = 0dis2[2]=dis1[1]+w[1][2]=−1+1=0 ;
dis2[3]=dis1[2]+w[2][3]=−3dis_2[3] = dis_1[2] + w[2][3] = -3dis2[3]=dis1[2]+w[2][3]=−3 ;
dis2[1]=dis1[3]+w[3][1]=−3+(−4)=−7dis_2[1] = dis_1[3] + w[3][1] = -3 + (-4) = -7dis2[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 中一个点可能在出队列之后再次被放入队列,也就是说一个点修改过其它的点之后,过了一段时间可能会获得更短的路径,于是再次用来修改其它的点,这样反复进行下去;
对于负环时,则负环上的节点会一直进行松弛操作,即一直进队,则判断当有节点 nnn 次进入队时,则有负环;
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短路可用类似 SPFASPFASPFA 算法的方法,即当第K次搜索到终点时,就为第K短路;
由于题目给出了起点与终点,可考虑用A*算法优化;
估价函数,
根据估价函数的设计准则,当前点 xxx 到 eee 点的估计距离应不大于 xxx 到 eee 点的实际距离,则把估价函数定为从 xxx 到 eee 的最短路长度;
则程序思路如下
-
输入时,正反向建两个图,用反向建的图处理结点 xxx 到终点 eee 点的最短路;
-
从起点开始A*搜索扩展状态,当第K次搜索到 eee 结点时,就得到路径长;
当起点为终点时,会把起点算作一次,所以次数+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(n2)O(n^2)O(n2) | 不适合负权及负权回路 |
SPFA | 单源最短路径 | O(e)O(e)O(e) | 不适合负权回路 |
Bellman-Ford | 单源最短路径 | O(ne)O(ne)O(ne) | 不适合负权回路 |
Floyd | 多源最短路径 | O(n3)O(n^3)O(n3) | 不适合负权回路 |