常用最短路径算法总结如下:
针对单源最短路中存在负权边的问题,常用的两个算法分别为Bellman-Ford算法和SPFA算法
1.Bellman-Ford
算法伪代码:
for n 次
for 所有边a,b,w
dist[b] = min(dist[b], dist[a] + w)
时间复杂度O(mn)
注:Bellman-Ford的最外层循环n,表示最多不经过n条边的最短路径
下面看一道示例题
问题描述
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你求出从 1 号点到 n 号点的最多经过 k 条边的最短距离,如果无法从 1 号点走到 n 号点,输出 impossible
。
注意:图中可能 存在负权回路 。
输入格式
第一行包含三个整数 n,m,k。
接下来 m 行,每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
点的编号为 1∼n。
输出格式
输出一个整数,表示从 1 号点到 n 号点的最多经过 k 条边的最短距离。
如果不存在满足条件的路径,则输出 impossible
。
数据范围
1≤n,k≤500,
1≤m≤10000,
1≤x,y≤n,
任意边长的绝对值不超过 10000。
输入样例
3 3 1
1 2 1
2 3 1
1 3 3
输出样例
3
代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 510, M = 10010;
int n, m, k;
int dist[N], backup[N];
struct Edge
{
int a, b, c;
}Edges[M];
void Bellman_ford(){
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for(int i = 0; i < k; i ++){
memcpy(backup, dist, sizeof backup);
for(int j = 0; j < m; j ++){
auto e = Edges[j];
dist[e.b] = min(dist[e.b], backup[e.a] + e.c);
}
}
}
int main(){
scanf("%d%d%d", &n, &m, &k);
for(int i = 0; i < m; i ++){
int x, y, z;
scanf("%d%d%d", &x, &y, &z);
Edges[i] = {x, y, z};
}
Bellman_ford();
int res = dist[n];
if(res > 0x3f3f3f3f / 2) printf("impossible");
else printf("%d", res);
return 0;
}
说明
Bellman-Ford算法在每次迭代时,需要对dist数组进行备份,目的是防止串联(即防止新更新的dist内的结点对其他结点距离的影响)
举个例子
初始状态dist数组如下
a | b | c |
0 | +∞ | +∞ |
第一次迭代
首先更新起点到b的距离
a | b | c |
0 | 1 | +∞ |
如果不备份的话,继续用dist数组更新,那么此时起点到c的距离会由dist[b]和bc更新为2
但Bellman-Ford实际迭代n次的含义是寻找不超过n条边的最短距离,显然这里a到c在n=1时,即不超过1条边的最短距离应该为3
所以需要在每次迭代时对dist进行备份,保证每次对所有结点的更新是由上一次的dist数组更新而来
2.SPFA算法
SPFA实际上是对Bellman-Ford算法进行优化
我们知道,在Bellman-Ford算法中,在每轮迭代都需要遍历所有边进行更新,SPFA就是在这个地方进行的优化
回顾一下Bellman-Ford中的更新公式dist[b] = min(dist[b], dist[a] + w)
上述公式只有在dist[a]发生更新的时候,dist[b]才有可能发生更新,因此只需要在a更新时执行上述式子即可,可以设立一个队列queue来保存待优化的节点,算法伪代码如下:
当队列不为空时:
取出队首元素t
利用t更新其所有邻接结点v的dist数组的值
若v不在队列中,则将v加入队列
用一个形象的示例来演示SPFA算法的流程
假设起点为a, 初始时首先将起点a入队,队列中此时状态为{a}
a | b | c | d | e | f |
0 | +∞ | +∞ | +∞ | +∞ | +∞ |
取出队首结点a,此时起点a到b和d的距离发生了更新,分别为6和4,则更新dist数组,并将b和d入队,队列中此时的状态为{b,d}
a | b | c | d | e | f |
0 | 6 | +∞ | 4 | +∞ | +∞ |
取出队首结点b,此时起点a到c的距离经过结点b发生了更新,距离为8,则更新dist数组,并将结点c入队,队列中此时的状态为{d,c}
a | b | c | d | e | f |
0 | 6 | 8 | 4 | +∞ | +∞ |
取出队首结点d,此时起点a到c和f的距离经过结点d发生了更新,dist[c]更新为5,dist[f]更新为7,因为c此时已在队列中,无需重复入队,将f加入队列,队列中此时的状态为{c,f}
a | b | c | d | e | f |
0 | 6 | 5 | 4 | +∞ | 7 |
取出队首结点c,此时起点a到e的距离经过结点c发生了更新,距离为12,则更新dist数组,并将结点e入队,队列中此时的状态为{f,e}
a | b | c | d | e | f |
0 | 6 | 5 | 4 | 12 | 7 |
取出队首结点f,此时起点a到e的距离经过结点f发生了更新,距离为9,则更新dist数组,因为此时结点e已在队列中,无需入队,队列中此时的状态为{e}
a | b | c | d | e | f |
0 | 6 | 5 | 4 | 9 | 7 |
取出队首节点e,此时起点a到各个结点的距离没有发生更新,队列为空,结束迭代
可以看到,在示例中,为了防止一个结点重复入队,可以添加一个状态数组,记录一个结点是否在队列中,提高算法效率
下面看一道具体的问题
问题描述
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 impossible
。
数据保证不存在负权回路。
输入格式
第一行包含整数 n 和 m。
接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。
如果路径不存在,则输出 impossible
。
数据范围
1≤n,m≤10^5,
图中涉及边长绝对值均不超过 10000。
输入样例
3 3
1 2 5
2 3 -3
1 3 4
输出样例
2
代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
const int N = 100010;
int n, m;
int h[N], w[N], e[N], ne[N], idx;
int dist[N];
bool st[N]; //记录某个结点是否在队列中
void add(int a, int b, int c){
w[idx] = c;
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++;
}
int spfa(){
memset(dist, 0x3f, sizeof dist);
queue<int> q;
dist[1] = 0;
q.push(1);
st[1] = true;
while(q.size()){
auto t = q.front();
q.pop();
st[t] = false;
for(int i = h[t]; i != -1; i = ne[i]){
int j = e[i];
if(dist[j] > dist[t] + w[i]){
dist[j] = dist[t] + w[i];
if(!st[j]){
st[j] = true;
q.push(j);
}
}
}
}
return dist[n];
}
int main(){
memset(h, -1, sizeof h);
scanf("%d%d", &n, &m);
for(int i = 0; i < m; i ++){
int x, y, z;
scanf("%d%d%d", &x, &y, &z);
add(x, y, z);
}
int res = spfa();
if(res > 0x3f3f3f3f / 2) printf("impossible");
else printf("%d", res);
return 0;
}
补充
SPFA和Bellman-Ford算法因为是处理负权图的,一个基本的应用是判断一个图中是否存在负环
处理方式是维护一个额外的数组记录起点到每个点距离最短时所经过的边数,如果边数≥n(n个结点最多n-1条边),则证明图中存在负环(若有负环则最短距离迭代时会一直在负环处循环)
下面是一个示例
问题描述
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你判断图中是否存在负权回路。
输入格式
第一行包含整数 n 和 m。
接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
输出格式
如果图中存在负权回路,则输出 Yes
,否则输出 No
。
数据范围
1≤n≤2000,
1≤m≤10000,
图中涉及边长绝对值均不超过 10000。
输入样例
3 3
1 2 -1
2 3 4
3 1 -4
输出样例
Yes
代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
const int N = 100010;
int n, m;
int h[N], w[N], e[N], ne[N], idx;
int dist[N], cnt[N];
bool st[N]; //记录某个结点是否在队列中
void add(int a, int b, int c){
w[idx] = c;
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++;
}
bool spfa(){
queue<int> q;
for(int i = 1; i <= n; i ++){
q.push(i);
st[i] = true;
}
while(q.size()){
auto t = q.front();
q.pop();
st[t] = false;
for(int i = h[t]; i != -1; i = ne[i]){
int j = e[i];
if(dist[j] > dist[t] + w[i]){
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1;
if(cnt[j] >= n) return true;
if(!st[j]){
st[j] = true;
q.push(j);
}
}
}
}
return false;
}
int main(){
memset(h, -1, sizeof h);
scanf("%d%d", &n, &m);
for(int i = 0; i < m; i ++){
int x, y, z;
scanf("%d%d%d", &x, &y, &z);
add(x, y, z);
}
if(spfa()) printf("Yes");
else printf("No");
return 0;
}
3.Floyd算法
Floyd算法适用于多源最短路径问题,简短粗暴,更新过程只需四行代码,三重for循环加一行状态更新方程
Floyd算法的本质是基于动态规划,状态转移方程为
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j])
dp[i][j]表示结点i和j之间的最短距离,该距离可由i到k的距离加上k到j的距离来更新
算法伪代码如下:
for k 1→n:
for i 1→n:
for j 1→n:
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
时间复杂度:O(n³)
下面是一个例子
问题描述
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,边权可能为负数。
再给定 k 个询问,每个询问包含两个整数 x 和 y,表示查询从点 x 到点 y 的最短距离,如果路径不存在,则输出 impossible
。
数据保证图中不存在负权回路。
输入格式
第一行包含三个整数 n,m,k。
接下来 m 行,每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
接下来 k 行,每行包含两个整数 x,y,表示询问点 x 到点 y 的最短距离。
输出格式
共 k 行,每行输出一个整数,表示询问的结果,若询问两点间不存在路径,则输出 impossible
。
数据范围
1≤n≤200,
1≤k≤n²
1≤m≤20000,
图中涉及边长绝对值均不超过 1000010000。
输入样例
3 3 2
1 2 1
2 3 2
1 3 1
2 1
1 3
输出样例
impossible
1
代码
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int N = 210, INF = 1e9;
int n, m, k;
int dist[N][N];
void floyd(){
for(int k = 1; k <= n; k ++){
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= n; j ++){
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
}
}
}
}
int main(){
for(int i = 0; i < N; i ++){
for(int j = 0; j < N; j ++){
if(i == j) dist[i][j] = 0;
else dist[i][j] = INF;
}
}
scanf("%d%d%d", &n, &m, &k);
for(int i = 0; i < m; i ++){
int x, y, z;
scanf("%d%d%d", &x, &y, &z);
dist[x][y] = min(dist[x][y], z);
}
floyd();
while(k --){
int x, y;
scanf("%d%d", &x, &y);
if(dist[x][y] >= INF / 2) cout << "impossible" << endl;
else printf("%d\n", dist[x][y]);
}
return 0;
}
补充
图的表示方式有邻接矩阵表示法和邻接表表示法
稠密图用邻接矩阵,稀疏图用邻接表