当前位置:   article > 正文

图论 ——五种最短路算法

最短路算法

文章目录


前言

本篇文章讲的是图论里的最短路问题,如果你还没有图论的基础知识,可以看看我之前的文章:

DFS(深度优先算法)  BFS(广度优先算法)邻接表和邻接矩阵、树的遍历 (DFS和BFS)

这些都是关于图论的基本知识。


一、最短路是什么?

最短路径: 从某个点A(位置)到另一个点B(位置)的最短距离,实现方法:点A途中可以经过很多个点C,然后通过不断更新点A到途中点 C 的最短距离,最后实现最短距离到达 点B。

A -> C1 -> C2 -> C3 -> B

最短路径的分类:

单源最短路:图中的一个点到其余各点的最短路径

多源最短路:图中每两个点的最短路径

框架图解:(如果看不清的话,放大浏览器再观看)

 图中稠密图用邻接矩阵,稀疏图用邻接表,具体了解:

邻接表和邻接矩阵、树的遍历 (DFS和BFS)

二、朴素Dijkstra算法

Dijkstra算法(迪杰斯拉算法):该算法的特点是从起始点开始,采用贪心算法的策略,采用加点的的方式,每次遍历到起始点距离最近且从未被访问过的顶点的邻接节点t,将该点t加入集合S中,直到扩展到终点位置。

时间复杂度:O(n ^ 2)

思想(操作):

  • 将图上的点分为两个集合:分别是S集合N集合
    1. S:表示访问过的点(用st数组存储)
    2. N:表示未访问过的点
  • N集合中的点到S集合距离最短依次加入到S集合
  • 用刚到S集合中的点t去更新集合N起始点的距离(这一步也就是松弛操作)

图解:

步骤: dist[ ]:每个点到起始点的距离  st[ ]:是否加入到了s集合中

  1. 初始化距离:把每个点都初始化为0x3f3f3f3f(无穷大)
  2. 进行n层循环:遍历dist数组,找到一个不在S集合中并距离S集合最短的点t,每一层循环都将找到的点t将它放入S集合中(st[t] = true)
  3. 用找到的点t去更新N集合起始点的距离(松弛操作)

代码 + 注释:

  1. const int N = 1e5 + 10;//多少个点
  2. int dist[N]; //每个点到起始点的距离
  3. bool st[N]; //S集合
  4. void dijkstra(){
  5. memset(dist, 0x3f3f3f3f, sizeof dist);//初始化距离
  6. dist[1] = 0;
  7. for(int i = 1; i <= n; i ++){ //进行n次循环
  8. int t = -1; //设找到的点初始化为1
  9. for(int j = 1; j <= n; j ++){
  10. if(!st[j] && (t == -1 || dist[j] < dist[t]))
  11. //如果该点j没在S集合中并且没更新或者有距离S集合更小的点
  12. t = j; //找到该点
  13. }
  14. st[t] = true;加入集合S中
  15. //松弛操作,用该点更新到s的距离
  16. for(int j = 1; j <= n; j ++){
  17. dist[j] = min(dist[j], dist[t] + g[t][j]);
  18. }
  19. }
  20. if(dist[n] == 0x3f3f3f3f) puts("impossible");//如果为无穷大说明到不了n点
  21. else printf("%d", dist[n]);
  22. }

三、堆优化版Dijkstra算法

堆优化版Dijkstra算法堆优化版Dijkstra算法是对朴素Dijkstra算法遍历所有点比较找出距离最近的点这一步骤,使用小根堆(优先队列)对这段代码进行优化:

  1. for(int i = 1; i <= n; i ++){ //进行n次循环
  2. int t = -1; //设找到的点初始化为1
  3. for(int j = 1; j <= n; j ++){
  4. if(!st[j] && (t == -1 || dist[j] < dist[t]))
  5. //如果该点j没在S集合中并且没更新或者有距离S集合更小的点
  6. t = j; //找到该点
  7. }

时间复杂度:O(m * logn)

思想:                                ​​​​​typedef pair<int, int>PII;

用小根堆priority_queue<PII, vector<PII>, greater<PII>>heap;存储距离和点,堆自动排序,可以排序取距离S集合小的点,然后每次取不在集合S中距离最段的点,再进行松弛操作,最后将松弛操作的点插入小根堆中

步骤:

  1. 初始化距离:把每个点都初始化为0x3f3f3f3f(无穷大),并将1号点放在堆中
  2. 取出堆顶的点,用该点t进行拓展,采用邻接表的数据结构,遍历该点t能到的所有节点
  3. 进行松弛操作,然后把松弛的点距离加入堆中。

代码 + 注释:

  1. typedef pair<int, int>PII; //pair<int, int>用来存两个值
  2. const int N = 1e5 + 10;
  3. int dist[N];
  4. bool st[N];
  5. int dijkstra(){
  6. memeset(dist, 0x3f3f3f3f, sizeof dist);//初始化距离
  7. dist[1] = 0;
  8. priority_queue<PII, vector<PII>, greater<int>>heap;//定义小根堆
  9. heap.push({0, 1}); //一定要距离在第一位,因为小根堆是根据第一个数据来排序
  10. while(heap.size()){
  11. PII t = heap.top(); //取堆顶距离最小的元素
  12. heap.pop();
  13. int distance = t.first, ver = t.second;//取出距离和点
  14. if(st[ver]) continue;//如果该点以及加入了集合S中,就continue
  15. st[ver] = true; //否则加入该点
  16. for(int i = h[ver]; i != -1; i = ne[i]){ //遍历该点能到的点的位置
  17. int j = e[i];
  18. if(dist[j] > distance + w[i]){ //进行松弛操作
  19. dist[j] = distance + w[i];
  20. heap.push({dist[j], j}); //入堆
  21. }
  22. }
  23. }
  24. if(dist[n] == 0x3f3f3f3f) return -1;
  25. else return dist[n];
  26. }

四、Bellman_Ford算法

Bellman_Ford算法(贝尔曼-福特算法):该算法比Dijkstra算法更具有普遍性,Dijkstra算法采用的是贪心思想,而Bellman_Ford采用的是动态规划,因为它对边没有要求,可以处理负权边与负权回路,也可以求边数限制的最短路,缺点是它的时间复杂度比较高,不能判断负环

时间复杂度:O(n * m)

思想:结构体来存储图对所有的边(重点)进行n - 1轮松弛操作,就是第一轮对所有边进行松弛,得到的是源点最多经过一条边到达其他顶点的最短距离,第二轮对所有的边进行松弛,得到的是最多经过两条边到其他顶点的最短距离,以此类推,最后达到n

图解:

步骤:

  1. 循环n - 1次,每次循环更新每条边的最短距离
  2. 备份一份上次迭代dist距离的数据,防止串联
  3. 用以后的dist[j]进行拓展,松弛操作

代码 + 注释:

  1. const int N = 1e5 + 10;
  2. int dist[N];//距离
  3. int back[N];//备份的数据
  4. struct Edge{
  5. int a, b, w;
  6. }edge[N];
  7. int bellman_ford(){
  8. memset(dist, 0x3f3f3f3f, sizeof dist); //初始化距离
  9. dist[0] = 1;
  10. for(int = 0; i <= k; i ++){ //可以求只经过k条边(限制边数)
  11. memcpy(back, dist, sizeof dist); //备份防止串联
  12. for(int j = 1; j <= n; j ++){ //遍历每个点
  13. int a = edge[i].a, b = edge[i].b, w = edge[i].w; //取值
  14. dist[b] = min(dist[b], back[a] + w); //松弛操作
  15. }
  16. }
  17. if(dist[n] > 0x3f3f3f3f / 2) return -1; //因为存在负权边,所以0x3f3f3f3f要除2
  18. return dist[n];
  19. }

五、spfa算法

SPFA算法(全称Shortest Path Faster Algorithm):是Bellman_frod的队优化形式,通常用来求含负权边的的单源最短路问题,以及判断负权环,如果存在负权环就不能用SPFA算法计算最短路

SPFA算法与Bellman_frod的区别SPFABellman_ford队优化版,但Bellman_Ford可以用来求负环的最短路,是因为其循环次数是有限制的,因此不会发生死循环,而SPFA算法不可以求带有负环的最短路,由于用了队列存储,只要发生了更新就会不断的入队,因此有了负权回路就不能用SPFA否则会死循环,但是SPFA可以利用这点来判断图中是否存在负环,如果某个点(非终点)的经过边数达到了n就说明存在负环

时间复杂度:由于SPFA是Bellman_ford优化而来,所以SPFA的最坏的情况是O(n * m),一般情况下是O(n)

对Bellman_ford的代码优化:

  1. for(int j = 1; j <= n; j ++){ //遍历每个点
  2. int a = edge[i].a, b = edge[i].b, w = edge[i].w; //取值
  3. dist[b] = min(dist[b], back[a] + w); //松弛操作
  4. }

思路:​​​​​​​采用的是类似BFS无权环的思路,设立一个队列来保存待优化的结点,优化时每次取队头结点t,然后遍历队头能经过的边到达的点vt点对所能经过的边的点v进行松弛操作,如果能进行松弛操作,并且v点不在当前队列中,就将v点入队,这样不断进行松弛操作,直到队列为空为止

步骤:

  1. 初始化dist[ ]数组,建立一个队列,将起始点入队
  2. 取出队头进行扩展,并进行松弛操作

代码 + 注释:

  1. const int N = 1e5 + 10; //多少个点
  2. int e[N], ne[N], w[N], idx, h[N];
  3. void add(int a, int b, int c){ //邻接表的存储方式
  4. e[idx] = b;
  5. w[idx] = c;
  6. ne[idx] = h[a];
  7. h[a] = idx ++;
  8. }
  9. int spfa(){
  10. memset(dist, 0x3f3f3f3f, sizeof dist); //初始化距离
  11. dist[0] = 1;
  12. queue<int>q; //定义队列
  13. q.push(1);
  14. st[1] = true;
  15. while(q.size()){
  16. int t = q.front(); //取出队头
  17. q.pop();
  18. st[t] = false;
  19. for(int i = h[t]; i != -1; i = ne[i]){ //遍历t能到的点
  20. int j = e[i];
  21. if(dist[j] > dist[t] + w[i]){ //松弛操作
  22. dist[j] = dist[t] + w[i];
  23. if(!st[j]){
  24. q.push(j);
  25. st[j] = true;
  26. }
  27. }
  28. }
  29. }
  30. return dist[n];
  31. }

SPFA算法判读负环的代码 + 注释:

  1. const int N = 1e5 + 10;
  2. int dist[N];
  3. int cnt[N];记录当前点t到源点最短路的边数,
  4. bool spfa(){
  5. // 这里不需要初始化dist数组为 正无穷/初始化的原因是, 如果存在负环, 那么dist不管初始化为多少, 都会被更新
  6. queue<int>q;
  7. //不仅仅是第一个点了, 因为第一个点可能到不了有负环的点, 因此把所有点都加入队列
  8. for(int i = 1;i <= n; i ++){
  9. q.push(i);
  10. st[i] = true;
  11. }
  12. while(q.size()){
  13. int t = q.front();
  14. q.pop();
  15. st[t] = false;
  16. for(int i = h[t]; i != -1; i = ne[i]){
  17. int j = e[i];
  18. if(dist[j] > dist[t] + w[i]){
  19. dist[j] = dist[t] + w[i];
  20. cnt[j] = cnt[t] + 1;//如果能进行松弛操作就在当前点的cnt+ 1
  21. if(cnt[j] >= n){
  22. return true;
  23. }
  24. if(!st[j]){
  25. q.push(j);
  26. st[j] = true;
  27. }
  28. }
  29. }
  30. }
  31. return false;
  32. }

六、Floyd算法

Floyd算法(弗洛伊德算法又称插点法):采用动态规划的思想,来解决给多源最短路的问题,可以求图中的任意一点x到任意一点y的距离

时间复杂度:O(n ^ 3)        三重循环

算法思路:从图的带权邻接矩阵开始,进行n次迭代更新,每次更新每两个点之间的最短距离,状态方程:f[i][j] = min(f[i][j], f[i][k] + f[k][j]);

步骤:

  1. 初始化:从任意一条单边路径开始。所有两点之间的距离是边的权,如果两点之间没有边相连,则权为无穷大

  2. 对于每一对顶点 i 和 j, 看看是否存在一个顶点 k 使得从 i 到 k 再到 j 比已知的路径更短。如果是更新它。

代码 + 注释:

  1. void Folyd(){
  2. for (int i = 1; i <= n; i ++ ){ //初始化
  3. for(int j = 1; j <= n; j ++){
  4. if(i == j) d[i][j] = 0;
  5. else d[i][j] = INF;
  6. }
  7. }
  8. //动态规划
  9. for(int k = 1; k <= n; k ++)
  10. for (int i = 1; i <= n; i ++ )
  11. for (int j = 1; j <= n; j ++ )
  12. d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
  13. }


总结

以上就是我所了解的几个最短路算法,可能还有很多待优化的地方,希望大家能指正。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/不正经/article/detail/243695?site
推荐阅读
相关标签
  

闽ICP备14008679号