前言

如果你对图论相关知识一点也没有,那么建议您先去了解这些知识:https://acmer.blog.csdn.net/article/details/122310835,然后就可以快乐的学习最短路算法啦

视频中绘图软件:https://csacademy.com/app/graph_editor/

配套讲解视频:https://www.bilibili.com/video/BV1Fa411C7wX/

如果哪里讲的有问题欢迎在评论区指出,感谢支持!

一、Floyd算法

1.1简介

Floyd算法算是最简单的算法,没有之一。适用于任何图

不管有向无向,边权正负,但是最短路必须存在。

基于动态规划的思想

1.2复杂度

1.2.1时间复杂度

O(N3)O(N^3)O(N3)

1.2.2空间复杂度

O(N2)O(N^2)O(N2)

1.3优缺点

1.3.1优点

常数小,容易实现,思路简单,能处理大部分图

1.3.2缺点

复杂度较高、不能处理负环图

1.4算法原理

我们定义一个三维数组f[k][u][v]f[k][u][v]f[k][u][v]表示的是允许经过[1,k][1,k][1,k]的点的uuu到vvv的最小距离,换句话说从111到kkk这些点可以作为uuu到vvv的中间节点,当然没也可以不经过,很显然我们如果要求解uuu到vvv的最小距离那么就是f[n][u][v]f[n][u][v]f[n][u][v](假设当前的图中有n个点的话),那么我们考虑怎么来维护这个关系呢,首先初始化来说,f[0][u][v]f[0][u][v]f[0][u][v]先初始化为INF,如果有边连接的话,那么我们取一个min就好,还有就是如果u和v相等的话应该初始化为0,那么我们就能推出这个状态是如何转移的:

f[k][u][v]=min(f[k−1][u][v],f[k−1][u][k]+f[k−1][k][v])f[k][u][v] = min(f[k-1][u][v],f[k-1][u][k] + f[k-1][k][v])f[k][u][v]=min(f[k−1][u][v],f[k−1][u][k]+f[k−1][k][v])

我们对经过k点和不经过k点去一个min,那么我们的状态转移方程就构造好啦,下面给出代码

void Floyd(){for(int k = 1;k <= n; ++k)for(int i = 1;i <= n; ++i)for(int j = 1;j <= n; ++j)f[k][i][j] = min(f[k-1][i][j],f[k-1][i][k]+f[k-1][k][j]);
}

我们发现我们这个第一维的k其实最多能用到当前这一层以及上一层的状态,那么我们可以通过滚动数组优化将其去掉,那么新的代码即为:

void Floyd(){for(int k = 1;k <= n; ++k)for(int i = 1;i <= n; ++i)for(int j = 1;j <= n; ++j)f[i][j] = min(f[i][j],f[i][k]+f[k][j]);
}

关于第一维对结果无影响的证明:

我们注意到如果放在一个给定第一维 k 二维数组中,f[x][k]f[k][y] 在某一行和某一列。而 f[x][y] 则是该行和该列的交叉点上的元素。

现在我们需要证明将 f[k][x][y] 直接在原地更改也不会更改它的结果:我们注意到 f[k][x][y] 的涵义是第一维为 k-1 这一行和这一列的所有元素的最小值,包含了 f[k-1][x][y],那么我在原地进行更改也不会改变最小值的值,因为如果将该三维矩阵压缩为二维,则所求结果 f[x][y] 一开始即为原 f[k-1][x][y] 的值,最后依然会成为该行和该列的最小值。

故可以压缩。

模板题:多源最短路

代码实现

#include<bits/stdc++.h>
using namespace std;const int N = 2e2+10;
const int INF = 0x3f3f3f3f;int n,m,k;
int f[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)f[i][j] = min(f[i][j],f[i][k]+f[k][j]);
}int main()
{cin>>n>>m>>k;int u,v,w;for(int i = 1;i <= n; ++i)for(int j = 1;j <= n; ++j)f[i][j] = i==j?0:INF;for(int i = 1;i <= m; ++i){cin>>u>>v>>w;f[u][v] = min(f[u][v],w);}Floyd();while(k--){cin>>u>>v;if(f[u][v] > INF / 2) cout<<"impossible"<<endl;else cout<<f[u][v]<<endl;}return 0;
}

二、Bellman-Ford 算法

2.1简介

Bellman−FordBellman-FordBellman−Ford 算法是一种基于松弛(relaxrelaxrelax)操作的最短路算法,可以求出有负权的图的最短路,并可以对最短路不存在的情况进行判断。当然你可能没听过这个算法,但是应该听过另一个算法SPFASPFASPFA 算法,SPFASPFASPFA算法其实就是加入了队列优化的Bellman−FordBellman-FordBellman−Ford

2.2复杂度

2.2.1时间复杂度

O(NM)O(NM)O(NM)

2.2.2空间复杂度

邻接矩阵:O(N2)O(N^2)O(N2)

邻接表:O(M)O(M)O(M)

2.3优缺点

2.3.1优点

能够处理负权图、能处理边数限制的最短路

2.3.2缺点

复杂度不太理想,很容易被卡

2.4算法原理

2.4.1松弛操作

在介绍该算法前,先来介绍一下松弛操作,对于一个边(u,v)(u,v)(u,v),松弛操作对应下面的式子:dis[v]=min(dis[v],dis[u]+w(u,v))dis[v]=min(dis[v],dis[u]+w(u,v))dis[v]=min(dis[v],dis[u]+w(u,v))。也就是我们将源点到v点的距离更新的一个操作

也就是开始可能源点SSS到vvv的路径为S−>vS->vS−>v,如果说经过uuu点后再到vvv的权值比直接到v小那么我们就更新一下路径最小值,这就是松弛操作

2.4.2 具体流程

Bellman算法要做的事就是对于图中所有的边,我们都进行一次松弛操作,那么完成这整个操作的复杂度大概在O(M)O(M)O(M),然后我们就一直循环的进行这个操作,直到我们不能进行松弛操作为止,就说明我们的单源最短路以及全部求完,那么我们需要多少次这样的完整操作呢,在最短路存在的情况下,由于一次松弛操作会使最短路的边数至少+1 ,而最短路的边数最多为N−1N-1N−1 ,因此整个算法最多执行N−1N-1N−1轮松弛操作。故总时间复杂度为O(NM)O (NM)O(NM)。

2.4.3 负环问题

上面提到了我们在求最短路存在的情况最多执行N−1N-1N−1轮松弛操作,如果数据中出现了负环,那么我们在第N轮操作的时候也会更新

注意一点

以SSS点为源点跑 Bellman-Ford 算法时,如果没有给出存在负环的结果,只能说明从SSS点出发不能抵达一个负环,而不能说明图上不存在负环。因为这个图可能是不连通的,那么对于不连通的图我们应该建一个虚点或者称之为超级源点,让这个点连向每一个其他的点并且权值为0,然后再来跑bellman_fordbellman\_fordbellman_ford

2.4.4 算法图解

第x轮松弛操作 本轮松弛操作
1 dis[2] =1,dis[3]=4,dis[4]=6
2 dis[4]=3
3 无操作

模板题:https://ac.nowcoder.com/acm/contest/27274/E

2.5代码实现

#include<algorithm>
#include<cstring>
#include<iostream>
#include<cstdio>
#include<vector>const int INF = 0x3f3f3f3f;
const int N = 10000+10;using namespace std;struct Node{int u,v,w;
};
vector<Node> E;
int n,m,s,t;
int dis[N];void bellman_ford(int s){for(int i = 1;i <= n; ++i) dis[i] = INF;dis[s] = 0;for(int i = 1;i <= n; ++i)for(int j = 0;j < 2 * m; ++j) {int u = E[j].u,v = E[j].v,w = E[j].w;if(dis[v] > dis[u] + w)dis[v] = dis[u] + w;}
}int main()
{cin>>n>>m>>s>>t;int u,v,w;for(int i = 1;i <= m; ++i) {cin>>u>>v>>w;E.push_back({u,v,w});E.push_back({v,u,w});}bellman_ford(s);if(dis[t] >= INF / 2) cout<<"-1"<<endl;else cout<<dis[t]<<endl;}

2.6判负环实现

如果我们发现第NNN轮操作也更新了那么说明存在负权回路

#include<iostream>
#include<algorithm>
#include<cstring>using namespace std;
#define INF 0x3f3f3f3fconst int N = 2e6+10;int n,m,q,k;
struct Edge{int u,v,w;
}E[N];
int dis[N];bool bellman_ford(){for(int i = 1;i <= n; ++i)for(int j = 0;j < m; ++j) {int u = E[j].u,v = E[j].v,w=E[j].w;if(dis[v] > dis[u] + w){dis[v] = dis[u] + w;if(i == n)return true;}}return false;
}int main()
{cin>>n>>m;for(int i = 0;i < m; ++i) {int u,v,w;cin>>u>>v>>w;E[i]={u,v,w};}bool k = bellman_ford();if(k) cout<<"Yes"<<endl;else cout<<"No"<<endl;return 0;
}

三、SPFA

3.1简介

关于SPFA,它死了

3.2复杂度

3.2.1时间复杂度

理想复杂度为O(KM)O(KM)O(KM),这里的KKK可以看作一个常数

最坏为O(NM)O(NM)O(NM)但是一般情况下是跑不到这么多(除非出题人卡SPFA)

3.2.2空间复杂度

邻接表:O(M)O(M)O(M)

邻接矩阵:O(N2)O(N^2)O(N2)

3.3优缺点

3.3.1优点

好写、效率挺快(一般来说即不被卡的话),能处理几乎所有类型的图

3.3.2缺点

容易被网格菊花图卡成傻b

3.4算法原理

3.4.1思想

其实SPFASPFASPFA算法就是bllman_fordbllman\_fordbllman_ford算法加上了队列优化,我们在上面的bellman_fordbellman\_fordbellman_ford算法能知道我们实际上是将每一个边都松弛了N−1N-1N−1次,实际上我们没必要松弛每一个点,因为有些点实际上是不用松弛太多或者说不用松弛的,那么我们希望去掉一些无用的松弛操作,这个时候我们用队列来维护哪些点可能会需要松弛操作,这样就能只访问必要的边了。同样的由于SPFA是队列优化的bellman_fordbellman\_fordbellman_ford那么同样能处理负权回路的图

tips:

虽然在大多数情况下 SPFASPFASPFA 跑得很快,但其最坏情况下的时间复杂度为O(NM)O(NM)O(NM),将其卡到这个复杂度也是不难的,所以考试时要谨慎使用(在没有负权边时最好使用 DijkstraDijkstraDijkstra 算法,在有负权边且题目中的图没有特殊性质时,若 SPFASPFASPFA 是标算的一部分,题目不应当给出 Bellman−FordBellman-FordBellman−Ford 算法无法通过的数据范围)。

3.4.2流程

用dis数组记录源点到有向图上任意一点距离,其中源点到自身距离为0,到其他点距离为INF。将源点入队,并重复以下步骤:

  • 队首t出队,并将t标记为没有访问过,方便下次入队松弛
  • 遍历所有以队首为起点的有向边(t,j)(t,j)(t,j),若dis[j]>dis[t]+w(t,j)dis[j] > dis[t] + w(t,j)dis[j]>dis[t]+w(t,j),则更新dis[j]dis[j]dis[j]
  • 如果点jjj不在队列中,则jjj入队,并将j标记为访问过
  • 若队列为空,跳出循环;否则执行第一步

我们会发现SPFA的这个过程就和BFS是类似的,如果图是随机生成的,时间复杂度为 O(KM) (K可以认为是个常数,m为边数,n为点数)但是实际上SPFA的算法复杂度是 O(NM) ,可以构造出卡SPFA的数据,让SPFA超时。所以使用SPFASPFASPFA前一定要三思

3.4.3算法图解

不在队列的元素 在队列的元素 当前松弛操作
{2,3,4}\{2,3,4 \}{2,3,4} {1}\{ 1\}{1} dis[3]=4,dis[2]=2,dis[4]=6dis[3]=4,dis[2]=2,dis[4]=6dis[3]=4,dis[2]=2,dis[4]=6
{1}\{1 \}{1} {2,3,4}\{2,3,4 \}{2,3,4} dis[4]=3
{1,2}\{1,2 \}{1,2} {3,4}\{3,4 \}{3,4} 无操作
{1,2,3}\{1,2,3 \}{1,2,3} {4}\{4 \}{4} 无操作
{1,2,3,4}\{1,2,3,4 \}{1,2,3,4} {∅}\{\varnothing \}{∅} 无操作

3.5bellman-ford的其他优化

除了队列优化(SPFA)之外,Bellman-Ford 还有其他形式的优化,这些优化在部分图上效果明显,但在某些特殊图上,最坏复杂度可能达到指数级。

  • 堆优化:将队列换成堆,与 Dijkstra 的区别是允许一个点多次入队。在有负权边的图可能被卡成指数级复杂度
  • 栈优化:将队列换成栈(即将原来的 BFS 过程变成 DFS),在寻找负环时可能具有更高效率,但最坏时间复杂度仍然为指数级
  • LLLLLLLLL 优化:将普通队列换成双端队列,每次将入队结点距离和队内距离平均值比较,如果更大则插入至队尾,否则插入队首
  • SLFSLFSLF 优化:将普通队列换成双端队列,每次将入队结点距离和队首比较,如果更大则插入至队尾,否则插入队首
  • D´Esopo−PapeD´Esopo-PapeD´Esopo−Pape 算法:将普通队列换成双端队列,如果一个节点之前没有入队,则将其插入队尾,否则插入队首

既然有了优化,那么就会有相应的卡的方法,具体请看这一篇回答:https://www.zhihu.com/question/292283275/answer/484871888

模板题:https://ac.nowcoder.com/acm/contest/27274/E

3.6 SPFA最短路代码实现

#include<algorithm>
#include<cstring>
#include<iostream>
#include<cstdio>
#include<vector>
#include<queue>const int INF = 0x3f3f3f3f;
const int N = 10000+10;using namespace std;struct Node{int v,w;
};
vector<Node> E[N];
int n,m,s,t;
int dis[N];
bool vis[N];void SPFA(int s){for(int i = 1;i <= n; ++i) vis[i] = false,dis[i] = INF;queue<int> que;que.push(s);dis[s] = 0,vis[s] = true;while(!que.empty()){int t = que.front();que.pop();vis[t] = false;for(int i = 0,l = E[t].size();i < l; ++i) {int j = E[t][i].v;int k = E[t][i].w;if(dis[j] > dis[t] + k){dis[j] = dis[t] + k;if(!vis[j]){vis[j] = true;que.push(j);}}}}
}
int main()
{cin>>n>>m>>s>>t;int u,v,w;for(int i = 1;i <= m; ++i) {cin>>u>>v>>w;E[u].push_back({v,w});E[v].push_back({u,w});}SPFA(s);if(dis[t] >= INF / 2) cout<<"-1"<<endl;else cout<<dis[t]<<endl;}

3.7 SPFA判负环实现

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<queue>
#include<vector>
using namespace std;
#define PII pair<int,int>const int N = 2e6+10;
int n,m,q;vector<PII> E[N];
int dis[N],cnt[N];
bool vis[N];void spfa(){queue<int> que;for(int i = 1;i <= n; ++i) que.push(i),vis[i] = true;while(!que.empty()){int t = que.front();que.pop();vis[t] = false;//表明t这个点已经离开这个队列了for(int i = 0,l = E[t].size();i < l; ++i) {int j = E[t][i].first,k = E[t][i].second;if(dis[j] > dis[t] + k) {dis[j] = dis[t] + k;cnt[j] = cnt[t] + 1;if(cnt[j] >= n) {//找到负权边cout<<"Yes"<<endl;return;}if(!vis[j])//将j这个点重新加入队列que.push(j),vis[j] = true;}}}cout<<"No"<<endl;
}int main()
{   cin>>n>>m;int u,v,w;for(int i = 0;i < m; ++i) {cin>>u>>v>>w;E[u].push_back({v,w});}spfa();return 0;
}

3.8 SPFA判断正环

关于SPFASPFASPFA判断正环的可以参考这一题:https://blog.csdn.net/m0_46201544/article/details/123011318

四、Dijkstra算法

4.1简介

dijkstradijkstradijkstra是一种单源最短路径算法,时间复杂度上限为 O(n2)O(n^2)O(n2) (朴素),在实际应用中较为稳定 ; 加上堆优化之后更是具有O((n+m)log⁡2n)O((n+m)\log_{2}n)O((n+m)log2​n) 的时间复杂度,在稠密图中有不俗的表现.

Dijkstra(/ˈdikstrɑ/或/ˈdɛikstrɑ/)Dijkstra(/ˈdikstrɑ/或/ˈdɛikstrɑ/)Dijkstra(/ˈdikstrɑ/或/ˈdɛikstrɑ/)算法由荷兰计算机科学家 E.W.DijkstraE. W. DijkstraE.W.Dijkstra 于 195619561956 年发现,195919591959 年公开发表。是一种求解 非负权图 上单源最短路径的算法。

贪心思想

4.2复杂度

4.2.1空间复杂度

4.2.1.1朴素Dijkstra

O(N2)O(N^2)O(N2)

4.2.1.2链式前向星优化+Dijkstra

O(M)O(M)O(M)

4.2.2时间复杂度

4.2.2.1 朴素Dijkstra

O(N2)O(N^2)O(N2)

4.2.2.2 链式前向星+堆优化的Dijkstra

O((n+m)log⁡2n)O((n+m)\log_{2}n)O((n+m)log2​n)

4.3优缺点

4.3.1 优点

朴素DijkstraDijkstraDijkstra和堆优化的DijkstraDijkstraDijkstra基本上能解决所有的正权图最短路问题,时间复杂度不会受到限制

4.3.2 缺点

不能处理负权图,如果需要处理负权图请移步SPFASPFASPFA

但是在某些特定的含有负边的图DJ也是对的例如:

但是我们稍加变换,迪杰斯特拉就不能处理了:

4.4 算法原理

4.4.1思想

DijkstraDijkstraDijkstra的核心思想其实就是贪心思想,每次寻找一个临近点的dis值最小的点,然后我们再来对该点进行松弛操作

4.4.2 流程

将结点分成两个集合:已确定最短路长度的点集(记为SSS集合)的和未确定最短路长度的点集(记为TTT集合)。一开始所有的点都属于TTT集合。

  • 1.初始化 dis[start]=0dis[start] = 0dis[start]=0, 其余节点的 disdisdis 值为无穷大
  • 2.从TTT集合中选取一个从源点到该点的最短路值最小的点xxx,然后放入SSS集合中(我们可以通过vis数组标记来实现集合划分)
  • 3.遍历 xxx 的所有出边 (x,y,z)(x,y,z)(x,y,z), 若 dis[y]>dis[x]+zdis[y] > dis[x] + zdis[y]>dis[x]+z, 则令 dis[y]=dis[x]+zdis[y] = dis[x] + zdis[y]=dis[x]+z
  • 4.重复 2,3 两步,直到所有点都加入集合SSS .
  • 时间复杂度为 O(n2)O(n^2)O(n2)

4.4.3 算法图解

T集合元素 S集合元素 当前松弛操作
{2,3,4}\{2,3,4\}{2,3,4} {1}\{1 \}{1} dis[1] = 0,dis[2]=1,dis[3]=4,dis[4]=6
{3,4}\{3,4 \}{3,4} {1,2}\{1,2\}{1,2} dis[4] = 3
{3}\{ 3\}{3} {1,2,4}\{1,2,4\}{1,2,4}
∅\varnothing∅ {1,2,3,4}\{1,2,3,4\}{1,2,3,4}

那么最终我们的dis值就变成了:

dis[1]=0dis[2]=1dis[3]=4dis[4]=3dis[1] = 0 \\ dis[2] = 1 \\ dis[3] = 4 \\ dis[4] = 3dis[1]=0dis[2]=1dis[3]=4dis[4]=3

4.5 优化

我们发现,对于寻找disdisdis值最小的点的操作,我们通过不同的方式维护的话那么算法的整体复杂度就会不同

  • 暴力:不使用任何数据结构进行维护,每次 2 操作执行完毕后,直接在TTT集合中暴力寻找最短路长度最小的结点。3操作总时间复杂度为O(M)O(M)O(M),2操作总时间复杂度为O(N2)O(N^2)O(N2),全过程的时间复杂度为O(N2+M)=O(N2)O(N^2+M)=O(N^2)O(N2+M)=O(N2)。
  • 二叉堆:每成功松弛一条边(u,v)(u,v)(u,v),就将vvv插入二叉堆中(如果vvv已经在二叉堆中,直接修改相应元素的权值即可),2操作直接取堆顶结点即可。共计O(M)O(M)O(M)次二叉堆上的插入(修改)操作,O(N)O(N)O(N)次删除堆顶操作,而插入(修改)和删除的时间复杂度均为O(log2N)O(log_2N)O(log2​N),时间复杂度为O((N+M)×log2N)=O(mlog2m)O((N+M)\times log_2N) = O(mlog_2m)O((N+M)×log2​N)=O(mlog2​m)。
  • 优先队列:和二叉堆类似,但使用优先队列时,如果同一个点的最短路被更新多次,因为先前更新时插入的元素不能被删除,也不能被修改,只能留在优先队列中,故优先队列内的元素个数是O(M)O(M)O(M)的,时间复杂度为 O(Mlog2M)O(Mlog_2M)O(Mlog2​M)
  • Fibonacci 堆:和前面二者类似,但 Fibonacci 堆插入的时间复杂度为O(1)O(1)O(1),故时间复杂度为O(Nlog2N+M)=O(Nlog2N)O(Nlog_2N+M)=O(Nlog_2N)O(Nlog2​N+M)=O(Nlog2​N),时间复杂度最优。但因为 Fibonacci 堆较二叉堆不易实现,效率优势也不够大,算法竞赛中较少使用。

注意的是:在稠密图中通过暴力方式维护效率更好,O(N2)O(N^2)O(N2),在稀疏图中通过堆优化的方式维护效率更高,O((n+m)log⁡2n)O((n+m)\log_{2}n)O((n+m)log2​n)

4.6 正确性证明(引自算法导论)

dijkstradijkstradijkstra 为什么是正确的呢?,当我们存储的所有的边都是正权边时,整个图的最小值不可能再被其他节点更新,所以我们在T集合中寻找dis最小值其实就是再选择全局最小值,也就是贪心的思想

下面用数学归纳法证明,在 所有边权值非负 的前提下,Dijkstra 算法的正确性。

简单来说,我们要证明的,就是在执行 1 操作时,取出的结点uuu最短路均已经被确定,即满足D(u)=dis(u)D(u)=dis(u)D(u)=dis(u) 。

  • 初始的时候S=∅S=\varnothingS=∅ ,假设成立,接下来使用反证法。

  • 设uuu点为算法中第一个在加入SSS集合时不满足D(u)=dis(u)D(u)=dis(u)D(u)=dis(u)的点。因为sss点一定满足D(u)=dis(u)=0D(u)=dis(u)=0D(u)=dis(u)=0,且它一定是第一个加入SSS集合的点,因此将uuu加入SSS集合前S!=∅S != \varnothingS!=∅,如果不存在sss到uuu的路径,则D(u)=dis(u)=+∞D(u)=dis(u)=+∞D(u)=dis(u)=+∞ ,与假设矛盾。

  • 于是一定存在路径s−>x−>y−>us->x->y->us−>x−>y−>u,其中y为s−>us->us−>u路径上第一个属于TTT集合的点,而xxx为yyy的前驱节点(显然x∈S)。需要注意的是,可能存在s=xs=xs=x或者y=uy=uy=u的情况,即s−>xs->xs−>x或者y−>uy->uy−>u是一个不存在的路径

  • 因为在uuu结点之前加入的结点都满足D(u)=dis(u)D(u)=dis(u)D(u)=dis(u),所以在xxx点加入到SSS集合时,有D(u)=dis(u)D(u)=dis(u)D(u)=dis(u),此时边(x,y)(x,y)(x,y)会被松弛,从而可以证明,将uuu加入到SSS时,一定有D(y)=dis(y)D(y)=dis(y)D(y)=dis(y)。

  • 下面证明D(u)=dis(u)D(u)=dis(u)D(u)=dis(u)成立。在路径s−>x−>y−>us->x->y->us−>x−>y−>u中,因为图上所有边边权非负,因此D(y)<=D(u)D(y)<= D(u)D(y)<=D(u)。从而dis(y)<=D(y)<=D(u)<=dis(u)dis(y)<=D(y)<=D(u)<=dis(u)dis(y)<=D(y)<=D(u)<=dis(u)。但是因为uuu结点在2 过程中被取出TTT集合时,yyy结点还没有被取出TTT集合,因此此时有dis(u)<=dis(y)dis(u)<=dis(y)dis(u)<=dis(y),从而得到dis(y)=D(y)=D(u)=dis(u)dis(y)=D(y)=D(u)=dis(u)dis(y)=D(y)=D(u)=dis(u),这与D(u)!=dis(u)D(u)!=dis(u)D(u)!=dis(u)的假设矛盾,故假设不成立。

  • 因此我们证明了,2 操作每次取出的点,其最短路均已经被确定。命题得证。

4.7 代码实现

模板题:https://ac.nowcoder.com/acm/contest/27274/E

4.7.1 朴素Dijkstra(稠密图)

#include<cstdio>
#include<algorithm>
#include<vector>
#include<iostream>
#include<cstring>using namespace std;
#define INF 0x3f3f3f3f
const int N = 1e3+10;int f[N][N],n,m,dis[N];
bool vis[N];void DJ(int s){for(int i = 1;i <= n; ++i) dis[i] = INF,vis[i] = false;dis[s] = 0;for(int i = 1;i <= n; ++i) {int t = -1;for(int j = 1;j <= n; ++j) if(!vis[j] && (t == -1 || dis[j] < dis[t])) t = j;if(t == -1) return;vis[t] = true;for(int j = 1;j <= n; ++j)if(dis[j] > dis[t] + f[t][j])dis[j] = dis[t] + f[t][j];}
}int main()
{int s,t;cin>>n>>m>>s>>t;int u,v,w;memset(f,0x3f,sizeof f);for(int i = 1;i <= m; ++i){cin>>u>>v>>w;f[v][u] = f[u][v] = min(f[u][v],w);}DJ(s);if(dis[t] == INF) cout<<"-1"<<endl;else cout<<dis[t]<<endl;return 0;
}

4.7.2 优先队列优化Dijkstra(稀疏图)

#include<cstdio>
#include<algorithm>
#include<vector>
#include<iostream>
#include<cstring>
#include<queue>using namespace std;
#define endl "\n"
#define PII pair<int,int>
#define INF 0x3f3f3f3f
const int N = 2e6+10;int dis[N],n,m;
bool vis[N];
vector<PII> E[N];void DJ(int s){for(int i = 1;i <= n; ++i) dis[i] = INF,vis[i] = false;priority_queue<PII,vector<PII>,greater<PII> > que;que.push({0,s});dis[s] = 0;while(!que.empty()){int t = que.top().second;que.pop();if(vis[t]) continue;vis[t] = true;for(int i = 0,l = E[t].size();i < l; ++i) {int j = E[t][i].first,w = E[t][i].second;if(dis[j] > dis[t] + w){dis[j] = dis[t] + w,que.push({dis[j],j});}}}
}int main()
{ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);int s,t;cin>>n>>m>>s>>t;int u,v,w;for(int i = 1;i <= m; ++i){cin>>u>>v>>w;E[u].push_back({v,w});E[v].push_back({u,w});}DJ(s);if(dis[t] == INF) cout<<"-1"<<endl;else cout<<dis[t]<<endl;return 0;
}

4.7.3 链式前向星+优先队列优化Dijkstra

#include<cstdio>
#include<cstring>
#include<queue>//
using namespace std;
const int N=2e5+5;//数据范围
struct edge{//存储边int u,v,w,next;//u为起点,v为终点,w为权值,next为前继
};
edge e[N];
int head[N],dis[N],n,m,s,cnt;//head为链中最上面的,dis表示当前答案,n为点数,m为边数,s为起点,cnt记录当前边的数量
bool vis[N];//vis表示这个点有没有走过
struct node{int w,to;//w表示累加的权值,to表示到的地方bool operator <(const node &x)const{//重载“<”号return w>x.w;}
};
priority_queue<node>q;//优先队列(堆优化)
void add(int u,int v,int w){++cnt;//增加边的数量e[cnt].u=u;//存起点e[cnt].v=v;//存终点e[cnt].w=w;//存权值e[cnt].next=head[u];//存前继head[u]=cnt;//更新链最上面的序号
}//链式前向星(加边)
void Dijkstra(){memset(dis,0x3f3f3f3f,sizeof(dis));//初始化,为dis数组附一个极大值,方便后面的计算dis[s]=0;//起点到自己距离为0q.push(node{0,s});//压入队列while(!q.empty()){//队列不为空node x=q.top();//取出队列第一个元素q.pop();//弹出int u=x.to;//求出起点if(vis[u]) continue;//已去过就不去了vis[u]=true;//标记已去过for(int i=head[u];i;i=e[i].next){int v=e[i].v;//枚举终点if(dis[v]>dis[u]+e[i].w){//若中转后更优,就转dis[v]=dis[u]+e[i].w;//更新q.push(node{dis[v],v});//压入队列}}}
}
int main(){int u,v,w = 1;s = 1;scanf("%d%d",&n,&m);//输入for(int i=1;i<=m;++i){scanf("%d%d",&u,&v);add(u,v,w);add(v,u,w);}Dijkstra();//DJprintf("%d\n",dis[n]);//输出1-n的最短路return 0;
}

4.8 路径打印问题

我们可以定义一个preprepre数组,然后pre[i]pre[i]pre[i]记录的是上一个位置是哪一个节点,当然初始的时候我们全部初始化为−1-1−1,然后每次松弛操作的时候就更新一下上一个节点的位置,你有没有发现这就是链式前向星,然后最后打印的时候要么递归打印,那么手动写栈打印,这个方法不只是适用于Dijkstra,而且也适用于其他最短路算法,如SPFASPFASPFA、bellman_fordbellman\_fordbellman_ford、FloydFloydFloyd等等

那么简单描述一下打印函数

void print(int t){for(int i = t;~i;i=pre[i]){cout<<i;if(i != s) cout<<" -> ";}
}

4.9 路径统计问题

其实我们在松弛操作的时候就能记录or更新从源点到当前点的路径条数,模板题可以参见下面的:最短路计数

五、Johnson 全源最短路径算法

待补充

训练题单

题目名称 题目链接 题解博客
线路 https://ac.nowcoder.com/acm/contest/27274/E https://blog.csdn.net/m0_46201544/article/details/122545202
【模板】单源最短路径(弱化版) https://www.luogu.com.cn/problem/P3371 模板请参见上面
【模板】单源最短路径(标准版) https://www.luogu.com.cn/problem/P4779 模板请参见上面
邮递员送信 https://www.luogu.com.cn/problem/P1629 https://acmer.blog.csdn.net/article/details/123031493
有边数限制的最短路 https://www.acwing.com/problem/content/855/ https://acmer.blog.csdn.net/article/details/122857119
spfa求最短路 https://www.acwing.com/problem/content/853/ https://acmer.blog.csdn.net/article/details/122857137
多源最短路 http://acm.mangata.ltd/p/P1507 https://acmer.blog.csdn.net/article/details/122857180
Frogger http://poj.org/problem?id=2253 https://acmer.blog.csdn.net/article/details/122998051
Heavy Transportation http://poj.org/problem?id=1797 https://acmer.blog.csdn.net/article/details/122998486
Silver Cow Party http://poj.org/problem?id=3268 https://acmer.blog.csdn.net/article/details/123002888
Wormholes http://poj.org/problem?id=3259 https://acmer.blog.csdn.net/article/details/123007502
Currency Exchange http://poj.org/problem?id=1860 https://acmer.blog.csdn.net/article/details/123011318
MPI Maelstrom http://poj.org/problem?id=1502 https://acmer.blog.csdn.net/article/details/123011950
Cow Contest http://poj.org/problem?id=3660 https://acmer.blog.csdn.net/article/details/123017032
Invitation Cards http://poj.org/problem?id=1511 https://acmer.blog.csdn.net/article/details/123017419
Candies http://poj.org/problem?id=3159 https://acmer.blog.csdn.net/article/details/123018565
面基 https://ac.nowcoder.com/acm/contest/27150/J https://blog.csdn.net/m0_46201544/article/details/122513361
On Average They’re Purple https://ac.nowcoder.com/acm/contest/12606/H https://blog.csdn.net/m0_46201544/article/details/122810241
最短路计数 https://www.luogu.com.cn/problem/P1144 https://acmer.blog.csdn.net/article/details/123031941

算法小讲堂之最短路算法(Floyd+bellman+SPFA+Dijkstra)相关推荐

  1. spfa算法_10行实现最短路算法——Dijkstra

    今天是算法数据结构专题的第34篇文章,我们来继续聊聊最短路算法. 在上一篇文章当中我们讲解了bellman-ford算法和spfa算法,其中spfa算法是我个人比较常用的算法,比赛当中几乎没有用过其他 ...

  2. 算法小讲堂之你真的会双指针吗?

    长文预告!!! 双指针算法 双指针又被称为 尺取法 双指针是一种简单而又灵活的技巧和思想,并不是一种具体的算法,单独使用可以轻松解决一些特定问题,和其他算法结合也能发挥多样的用处. 双指针顾名思义,同 ...

  3. 【算法小讲堂】数位dp(简单入门)

    数位打牌 爷爷,你没有关注的博主又更新博客啦!! 数位dp(打牌),这是一个相当深刻并且具有意义的话题.在没看懂这个内容的时候完完全全就是一脸懵逼,现在依旧是一脸懵逼.你以为你会了,题目:不,你不会! ...

  4. 算法小讲堂之B树和B+树(浅谈)|考研笔记

    文章目录 一.前言 二.定义 三.原理 3.1 树的高度 3.2 查找操作 3.3 插入操作 3.4 删除操作 3.4.1 删除关键字是非叶子结点 3.4.2 删除关键字是叶子结点 四.B+树 五.应 ...

  5. 算法小讲堂之哈希表|散列表|考研笔记

    文章目录 一. 基本概念 二. 哈希函数|散列函数 2.1 直接定址法 2.2 保留余数法 2.3 数字分析法 2.4 平方取中法 2.5 折叠法 2.6 随机数法 三.冲突处理 3.1 开放定址法 ...

  6. 算法小讲堂之平衡二叉树|AVL树(超详细~)

    文章目录 一.前言 二.定义 三.原理 3.1 查找操作 3.2 最小(大)值结点 3.3 旋转操作 3.3.1 LL旋转 3.3.2 RR旋转 3.3.3 LR旋转 3.3.4 RL旋转 3.4 插 ...

  7. dijkstra算法matlab程序_编程习题课 | 用最短路算法为你的小地图导航

    简介:路网拓扑的正确导入方式,运筹学算法的完整实战案例,最详细的代码讲解与分享. 引言:在研究路径选择和流量分配等交通问题时,常常会用到最短路算法.用最短路算法解决交通问题存在两个难点:一.算法的选择 ...

  8. zuc算法代码详解_最短路算法-dijkstra代码与案例详解

    引言 在研究路径选择和流量分配等交通问题时,常常会用到最短路算法.用最短路算法解决交通问题存在两个难点: 一.算法的选择和程序的编写.最短路算法有很多种改进算法和启发式算法,这些算法的效率不同,适用的 ...

  9. 坐在马桶上看算法:只有五行的Floyd最短路算法

    坐在马桶上看算法:只有五行的Floyd最短路算法 此算法由Robert W. Floyd(罗伯特·弗洛伊德)于1962年发表在"Communications of the ACM" ...

最新文章

  1. python一般用什么编译器-Python常用的编辑器有哪些?老男孩Python
  2. CTFshow php特性 web128
  3. Google ToolBar 3.0 Beta试用
  4. voms下的反射大师_VOMS旧版
  5. windows 安装PyAudio库
  6. 【华为云技术分享】用人工智能技术推动西安民俗文化,斗鱼超管团队有一套
  7. swiftui动画之tab自定义切换动画_Unity动画系统详解1:在Unity中如何制作动画?
  8. 20200118每日一句
  9. 如何彻底修改SQL server的数据库名
  10. 想要导航提示页最新安卓区_2020年网站页头设计:最佳实践及案例
  11. 漫谈多模光纤类型:OM1、OM2、OM3、OM4、OM5,深度好文,值得阅读
  12. android 通知写法_Android消息通知-Notifation
  13. pyraformer: low-complexity pyramidal attention for long-range time series modeling and forecasting
  14. 练T25- focus必看!所有成功截图汇总
  15. 天载配资解析天赐材料:目标180
  16. 性能测试5-性能测试环境搭建
  17. Windows下通过注册表修改某个类型文件的默认打开方式和文件图标
  18. MySQL之账号管理、建库以及四大引擎
  19. LeetCode:438. 找到字符串中所有字母异位词(简单易懂)
  20. 创建一个网页的基本步骤

热门文章

  1. 有什么游戏蓝牙耳机推荐?双十一热门游戏蓝牙耳机推荐
  2. 【Luogu】CF688B Lovely Palindromes
  3. Zookeeper源码解析-Leader/Follower节点的启动
  4. SpringBoot Validator
  5. Evince 0.7
  6. 2022年全球市场封闭式药物转移系统总体规模、主要企业、主要地区、产品和应用细分研究报告
  7. C# wcf动态使调用
  8. WCF服务部署到IIS上的步骤
  9. python画图显示中文_Python的matplotlib库画图不能显示中文问题解决
  10. C++比C多了什么(一)