总结一下图论里的最短路。
Dijkstra 算法
Dijkstra 算法可用于非负权值的图,可以表示为如下流程。
- 将源点的距离设为 0,其他点的距离设为无穷大。
- 从图中找出距离最小且未标记的点 u,并标记点 u。松弛所有与 u 相邻的节点。(松弛:尝试将 s→v 的路径用 s→u→v 的距离更新,即
dis[v]=min(dis[v],dis[u]+w[u][v])
)
- 重复第二步,直到所有节点都被标记。
如果需要输出路径,可以在松弛成功时记录每个节点的前驱。
根据在第二步找出距离最小的点的处理方法不同,Dijkstra 算法有不同的复杂度。
暴力实现
依次遍历所有 n 个节点,找出最小权值。这样第二步的复杂度为 O(n),总共执行 n 次,总复杂度为 O(n2)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| const int N = 1e5+5; const int INF = 0x3f3f3f3f;
struct Edge{ int v,w; };
int dis[N],pre[N]; vector<Edge> G[N]; bool vis[N];
int n,m; int s;
void dijkstra(){ memset(dis,0x3f,sizeof(dis)); dis[s]=0;
for(int i=0;i<n;i++){ int u;int du=INF; for(int j=1;j<=n;j++){ if(!vis[j] && dis[j]<du){ u=j,du=dis[j]; } }
vis[u]=1; for(auto edge:G[u]){ if(vis[edge.v])continue; if(dis[edge.v]>dis[u]+edge.w){ dis[edge.v]=dis[u]+edge.w; pre[edge.v]=u; } } } }
|
优先队列实现
优先队列实现在稀疏图上更有优势。
因为优先队列不能进行删除操作,所以队列中最多有 m 个节点(有之前松弛的距离较大的),复杂度为 O(mlogm)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| const int N = 1e5+5; const int INF = 0x3f3f3f3f;
struct Edge{ int v,w; };
int dis[N],pre[N]; vector<Edge> G[N]; bool vis[N];
int n,m,s;
struct Node{ int u,dis; bool operator<(const Node& rhs)const{ return dis>rhs.dis; } };
void dijkstra(){ memset(dis,0x3f,sizeof(dis)); dis[s]=0;
priority_queue<Node> q; q.push({s,0});
while(!q.empty()){ int u=q.top().u;q.pop(); if(vis[u])continue; vis[u]=1; for(auto edge:G[u]){ if(vis[edge.v])continue; if(dis[edge.v]>dis[u]+edge.w){ dis[edge.v]=dis[u]+edge.w; pre[edge.v]=u; } } } }
|
Bellman-Ford 算法
Bellman-Ford 算法可以表示为如下流程。
- 将源点的距离设为 0,其他点的距离设为无穷大。
- 对图中每条边执行松弛操作。
- 重复第 2 步,直到没有一条边松弛成功。
可以证明最多只需要松弛 n−1 遍。(因为每次松弛成功最短路的经过的边数就会增加 1,而最短路最多经过 n−1 条边,否则就会出现环)
每次执行松弛操作的复杂度为 O(m),故总复杂度为 O(nm)。
如果需要输出路径,可以在松弛成功时记录每个节点的前驱。
判断负环
Bellman-Ford 算法可以用来判断图中是否有负环。只需要判断 n−1 次松弛后,如果还能进行松弛操作,则代表有负环。
Bellman-Ford 算法判断负环只能代表从源点 s 出发可以抵达一个负环,如果图不强连通,不一定能找到图中的负环。因此,可以建立一个超级源点,从超级源点向每个节点建立一条边权为 0 的边,从超级源点开始进行 Bellman-Ford 算法即可保证判断负环不出错。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| const int N = 1e5+5; const int INF = 0x3f3f3f3f;
struct Edge{ int v,w; };
int dis[N],pre[N]; vector<Edge> G[N];
int n,m,s;
bool bellman_ford(){ memset(dis,0x3f,sizeof(dis)); dis[s]=0;
for(int i=1;i<n;i++){ for(int u=1;u<=n;u++){ for(auto edge:G[u]){ if(dis[edge.v]>dis[u]+edge.w){ dis[edge.v]=dis[u]+edge.w; pre[edge.v]=u; } } } }
for(int u=1;u<=n;u++){ for(auto edge:G[u]){ if(dis[edge.v]>dis[u]+edge.w){ return 1; } } } return 0; }
|
SPFA 算法
SPFA 算法可以看作是 Bellman-Ford 算法的一个改进。其基本思想是每次进行松弛操作时,只松弛与上次松弛成功的边相连的点。
使用一个队列记录上一轮循环中成功松弛的点。
关于 SPFA,他死了
SPFA 算法的最坏复杂度是 O(nm),意味着你不应该在一张非负权值图上使用 SPFA 算法,会被卡(应该用 dijkstra 算法)。因此 SPFA 算法应该只在图中存在负权边的情况下使用,视作一个 Bellman-Ford 算法的小优化。
判断负环
SPFA 算法也可以判断负环。只要同时记录一个点到源点的最短路经过了几条边。如果经过了大于等于 n 条边,则有负环。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| #include<bits/stdc++.h> using namespace std;
const int N = 1e5+5; const int INF = 0x3f3f3f3f;
struct Edge{ int v,w; };
int dis[N],pre[N]; vector<Edge> G[N]; int cnt[N]; bool in_queue[N];
int n,m,s;
bool spfa(){ memset(dis,0x3f,sizeof(dis)); dis[s]=0;
queue<int> q; q.push(s);in_queue[s]=1;
while(!q.empty()){ int u = q.front();q.pop(); in_queue[u]=0; for(auto edge:G[u]){ if(dis[edge.v]>dis[u]+edge.w){ dis[edge.v]=dis[u]+edge.w; pre[edge.v]=u; cnt[edge.v]=cnt[u]+1; if(cnt[edge.v]>=n)return 1; if(!in_queue[edge.v]){ q.push(edge.v);in_queue[edge.v]=1; } } } } return 0; }
|
Floyd 算法
一个多源最短路算法。使用三重循环实现。还可以用来找到图中的最小环。(最小环相关介绍见 最小环问题)
1 2 3 4 5 6 7 8 9 10 11
| void floyd(){ memset(dis,0x3f,sizeof(dis));
for (int k=1;k<=n;k++) { for (int i=1;i<=n;i++) { for (int j=1;j<=n;j++) { dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]); } } } }
|
总结
总结一下这四个算法。
|
Dijkstra 算法 |
Bellman-ford 算法 |
SPFA 算法 |
Floyd 算法 |
算法类型 |
单源最短路径 |
单源最短路径 |
单源最短路径 |
多源最短路径 |
图限制 |
非负权值图 |
- |
- |
- |
复杂度 |
暴力:O(n2) 优先队列:O(mlogm) |
O(nm) |
最坏 O(nm) |
O(n3) |
其余应用 |
- |
判断负环 |
判断负环 |
找最小环 |