文章目录

  • 最短路算法框架
  • 朴素Dijkstra算法
  • 堆优化版Dijkstra算法
  • Bellman-Ford算法
  • SPFA算法
    • SPFA判断负环
  • Floyd算法

最短路算法框架

最短路有五种算法,分别适用不同的情况。

单源最短路: 求一个点到其他点的最短路
多源最短路: 求任意两个点的最短路

稠密图用邻接矩阵存,稀疏图用邻接表存储。

稠密图: m 和 n2 一个级别
稀疏图: m 和 n 一个级别


朴素Dijkstra算法


集合s:所有已经确定最短路的点
①的意思就是在集合s外找一个距离起点最近的点
②的意思就是让这个最近点放到集合 s 中

Dijkstra算法是通过 n 次循环来确定 n 个点到起点的最短路的。

首先找到一个没有确定最短路且距离起点最近的点,并通过这个点将其他点的最短距离进行更新。每做一次这个步骤,都能确定一个点的最短路,所以需要重复此步骤 n 次,找出 n 个点的最短路。

核心代码

for(int i=0; i<n; i++){int t = -1;for(int j=1; j<=n; j++)   // 在没有确定最短路中的所有点找出距离最短的那个点 t if(!st[j] && (t == -1 || dist[t] > dist[j]))t=j;                  st[t]=true; // 代表 t 这个点已经确定最短路了for(int j=1; j<=n; j++) // 用 t 更新其他点的最短距离 dist[j] = min(dist[j],dist[t]+g[t][j]);}

代码讲解:

① 找到 t 后,t 是剩余未确定最短路中的距离起点最小的结点,那么它的最短距离我们就可以确定了,标记时让 st[t] = true 即可。

② t 是怎样更新其他点的最短距离呢?看下图


刚开始,t = 1,因为结点 1 距离起点距离为0,是距离最小的,我们拿1这个结点来更新其他结点的最短路,结点 2 的最短路可以更新为 1,因为 t 到起点的距离为 0,而1——>2的距离为 1,0+1=1,同理,结点 3 的最短距离更新为 4 。
这第一个结点的最短距离我们已经确定了,此时,再找下一个 t ,很明显,t = 2,我们再来用 2 这个结点来更新其他点的最短距离,因为 dist[2]+g[2][3] = 1 + 2,很明显 1 + 2 < 4,我们可以把结点 3 的最短距离更新成 3, 对应代码里的 dist[j] = min(dist[j],dist[t]+g[t][j]) 。

代码里每次更新都是循环1~n个结点,其实已经确定最短路的点是不用更新的了,还有 t 这个点可能与 j 这个点间是没有路的,也是不用更新的,不过这不影响答案,但你也可以更新时加个 if(!st[j] && g[t][j]!=0x3f3f3f3f)。

例题:
给定一个n个点m条边的有向图,图中可能存在重边和自环,所有边权均为正值。

请你求出1号点到n号点的最短距离,如果无法从1号点走到n号点,则输出-1。

输入格式
第一行包含整数n和m。

接下来m行每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。

输出格式
输出一个整数,表示1号点到n号点的最短距离。

如果路径不存在,则输出-1。

数据范围
1≤n≤500,
1≤m≤105,
图中涉及边长均不超过10000。

输入样例:
3 3
1 2 2
2 3 1
1 3 4

输出样例:
3

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 510;
int n,m;
int g[N][N]; // 邻接矩阵
int dist[N]; // 第 i 个点到起点的最短距离
bool st[N]; // 代表第 i 个点的最短路是否确定、是否需要更新 int dijkstra()
{memset(dist,0x3f,sizeof(dist)); // 将所有距离初始化正无穷dist[1] = 0;  // 第一个点到起点的距离为 0  for(int i=0; i<n; i++){int t = -1;for(int j=1; j<=n; j++)  // 在没有确定最短路中的所有点找出距离最短的那个点 if(!st[j] && (t == -1 || dist[t] > dist[j]))t=j;st[t]=true; //代表 t 这个点已经确定最短路了for(int j=1; j<=n; j++) // 用 t 更新其他点的最短距离 dist[j] = min(dist[j],dist[t]+g[t][j]);} if(dist[n] == 0x3f3f3f3f) return -1;  // 说明 1 和 n 是不连通的,不存在最短路 return dist[n];
}
int main()
{cin >> n >> m;memset(g,0x3f,sizeof(g));while(m--){int a,b,c;scanf("%d%d%d",&a,&b,&c);g[a][b] = min(g[a][b],c); // 保留长度最短的重边 }cout << dijkstra(); return 0;
}

堆优化版Dijkstra算法

优化版的Dijkstra算法是通过小根堆来找到当前堆中距离起点最短且没有确定最短路的那个点。

因为是稀疏图,所以需要用邻接表来存储。
还是上到题的题目,只不过数据范围变成1≤n,m≤1.5×105,这个数据范围如果还用朴素算法的话,O(n2)是1010级别,必定超时的,所以我们采用堆优化版的算法。

#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
typedef pair<int,int> PII; //first存距离,second存结点编号
const int N = 2e5+10;
int n,m;
int h[N],w[N],e[N],ne[N],idx; // 邻接表的存储
int dist[N]; // 第 i 个点到起点的最短距离
bool st[N]; // 代表第 i 个点的最短路是否确定、是否需要更新 void add(int a,int b,int c)
{e[idx] = b,w[idx] = c,ne[idx] = h[a],h[a] = idx++;
}int dijkstra()
{memset(dist,0x3f,sizeof(dist)); // 将所有距离初始化正无穷dist[1] = 0;  // 第一个点到起点的距离为 0  priority_queue<PII, vector<PII>, greater<PII>> heap; // 小根堆heap.push({0,1}); //把 1 号点放入堆中 while(heap.size()) // 堆不空{PII t = heap.top(); //找到当前距离最小的点heap.pop();int ver = t.second,distance = t.first; // ver为编号,distance为距离if(st[ver]) continue;   // 重边的话不用更新其他点了st[ver] = true;   //标记 t 已经确定最短路for(int i = h[ver]; i!=-1; i=ne[i])  // 用 t 更新其他点的最短距离{int j = e[i];if(dist[j] > distance + w[i]){dist[j] = distance + w[i];heap.push({dist[j],j}); //入堆}}}  if(dist[n] == 0x3f3f3f3f) return -1;  // 说明 1 和 n 是不连通的,不存在最短路 return dist[n];
}
int main()
{memset(h,-1,sizeof(h));cin >> n >> m;while(m--){int a,b,c;scanf("%d%d%d",&a,&b,&c);add(a,b,c);}cout << dijkstra(); return 0;
}

因为题目上是有重边的情况的,假设1——>2是有权重为2和3的重边,我们在用1号结点更新2号结点时,2号结点会两次入堆,这样我们发现堆中会有很多冗余的点,当堆中弹出下一个 t 时,t 是为{2,2}的,而不是{2、3},因为 2<3 ,这个时候我们就可以标记 2 结点为true了,等到下次堆中弹出{2、3}时,我们不需要用{2、3}来更新其他点了,因为我们已经用{2、2}这个距离更小的点更新过了,所以堆中当弹出{2、3}时直接continue即可。

解释一下Dijkstra算法为什么不能用于有负权的边的图:

因为Dijkstra算法是通过当前离起点最近的点来更新其他的点的距离,例如上图中的 4 号结点会被 2 号结点更新为2+1=3,但实际上4号结点的最短路径是3+(-2)=1,这样你就知道为什么Dijkstra算法不能用于有负权的边的图吧。


Bellman-Ford算法

Bellman-Ford算法是通过循环 n 次,每次循环都遍历每条边,进而更新结点的距离,每一次的循环至少可以确定一个点的最短路,所以循环 n次,就可以求出 n 个点的最短路。

for(int i=0; i<n; i++)for(int j=0; j<m; j++){if(dist[a]+w<dist[b])dist[b] = dist[a] + w; //w是a->b的权重 }

在这个算法上延申,如果我们想求有边数限制的最短路怎么求呢,假如让求从1号点到n号点的最多经过k条边的最短距离怎么求呢???

需要注意的是我们需要一个备份,先来看看为什么…

还是这张图,假设我们限制边数 k 为 1,那么外层循环只需要进行一次,肉眼可以看出,我们只能求出 1 和 2 和 3号结点的最短路,4号结点最短路是不存在的,可是当在枚举所有条边时,假如我们先枚举的1——>2边时,那么2号结点最短路被更新为2,这是没问题的,可是,当我们再枚举到2——>4边时,4号结点最短路会被更新为2+1=3,如果4后面还有结点的话,后面的所有结点都会被更新,可实际上,4结点是不存在最短路的,因为我们限制了 k。那么怎么解决呢,其实很简单,我们只需要用上一次的dist来更新即可,而我们把上一次更新后的dist放到备份里存起来以备下一次更新用。

有了备份,当枚举1——>2到时,2结点被更新为2,而枚举到2——>4时,4号结点是用2号结点上一次的dist来更新的,而2号结点上一次dist是 +∞,而 +∞ + 1 > +∞,所以说4号结点是不会被更新的。
但是你可能有疑问,2——>4的权值如果是-1,那3号结点是可以更新成 +∞ - 1的,那它不还是更新了?它确实更新了,但是,我们在最后判断时,还是会判断4号没有最短路的,这也就是为什么下道例题最后判断条件写成if(dist[n] > 0x3f3f3f3f/2) return -1。

例题:
853. 有边数限制的最短路
给定一个n个点m条边的有向图,图中可能存在重边和自环, 边权可能为负数。

请你求出从1号点到n号点的最多经过k条边的最短距离,如果无法从1号点走到n号点,输出impossible。

注意:图中可能 存在负权回路 。

输入格式
第一行包含三个整数n,m,k。

接下来m行,每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。

输出格式
输出一个整数,表示从1号点到n号点的最多经过k条边的最短距离。

如果不存在满足条件的路径,则输出“impossible”。

数据范围
1≤n,k≤500,
1≤m≤10000 ,
任意边长的绝对值不超过10000。

输入样例:
3 3 1
1 2 1
2 3 1
1 3 3

输出样例:
3

#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 510,M = 10010;int n,m,k;
int dist[N],backup[N]; //backup数组为上次
struct edges
{int a,b,w; // a->b权值为w的边
}edge[M];int bellman_ford()
{memset(dist,0x3f,sizeof(dist));dist[1] = 0;for(int i=0; i<k; i++) {memcpy(backup,dist,sizeof(dist)); //备份for(int j=0; j<m; j++)   // 枚举所有边 {int a = edge[j].a, b = edge[j].b, w=edge[j].w; dist[b] = min(dist[b],backup[a]+w); // 用备份更新 }}if(dist[n] > 0x3f3f3f3f/2) return -1;return dist[n];
}
int main()
{cin >> n >> m >> k;for(int i=0; i<m; i++){int a,b,w;cin >> a >> b >> w;edge[i] = {a,b,w};   }   int t = bellman_ford();if(t == -1) cout << "impossible";else cout << t;return 0;
}

SPFA算法

SPFA算法需要图中没有负环才能使用。其实大部分正权图也是可以用SPFA算法做的,例如最上面的那到题就可以用SPFA做,效率还高于Dijkstra算法。
SPFA算法是在Bellman-Ford的基础上优化后的算法。在Bellman-Ford算法中,如果某个点未被更新过,我们还是会用这个点去更新其他点,其实,该操作是不必要的,我们只需要拿更新过后的点去更新其他的点,因为只有用被更新过的点更新其他结点x,x的距离才可能变小。


核心代码:

int spfa()
{// bool st[N]: 存第 i 个点是不是在队列中,防止存重复的点 memset(dist,0x3f,sizeof(dist));dist[1] = 0;queue<int> q; //存储所有待更新的点q.push(1);  // 1号点入队 st[1] = true;while(q.size()) // 队列不空{int t = q.front(); //取队头 q.pop();st[t] = false; // 代表这个点已经不在队列了,因为存在边权为负数,某个点可能会被更新多次,所以可以多次入队和出队。for(int i = h[t]; i!=-1; i=ne[i]) // 更新 t 的所有临边结点的最短路 {int j = e[i];if(dist[j] > dist[t]+w[i]){dist[j] = dist[t] + w[i];if(!st[j])  //如果 j 不在队列,让 j 入队 {q.push(j); st[j] = true;  // 标记 j 在队中 }           }}  } if(dist[n] == 0x3f3f3f3f) return -1; // 不存在最短路 return dist[n];
}

SPFA判断负环

什么是负环呢? 下图左边的2——>3——>4就是一个负环,因为转一圈后的距离是负的,右图的 1 结点是应该自环,也属于负环。

相比上一个代码,多了一个cnt数组,cnt[x] 代表起点到x最短路所经的边数,当 cnt[x] ≥ n 时,则说明 1——>x 这条路径上至少经过 n 条边 ,那么也就是 1——>x 这条路径上至少经过 n+1 个点,而我们知道总共只有 n 个点,说明至少存在两个点是重复经过的,那么这个点构成的环一定是负环,因为只有负环才会让dist距离变小,否则我们为什么要两次经过同一个点呢。

int spfa()
{queue<int> q; for(int i=1; i<=n; i++) //将所有结点入队{st[i] = true;q.push(i);    }while(q.size()) // 队列不空{int t = q.front(); //取队头 q.pop();st[t] = false; // 代表这个点已经不在队列了for(int i = h[t]; i!=-1; i=ne[i]) // 更新 t 的所有临边结点的最短路 {int j = e[i];if(dist[j] > dist[t]+w[i]){dist[j] = dist[t] + w[i];cnt[j] = cnt[t] + 1; // t到起点的边数+1 if(cnt[j] >= n) return true;// 存在负环 if(!st[j])  //如果 j 不在队列,让 j 入队 {q.push(j); st[j] = true;  // 标记 j 在队中 }          }}  } return false;// 不存在负环
}

刚开始我们需要让所有点都入队,因为 1 这个结点可能跟我们要找的负环是不连通的,这样的话只通过 1 来是无法判断的,所以,我们让所有结点都入队。

dist数组是否初始化在这里是不影响的,因为我们要求的是是否存在负环,不是距离。


Floyd算法

Floyd算法是基于动态规划的,从结点 i 到结点 j 的最短路径只有两种:
1、直接 i 到 j
2、i 经过若干个结点到 k 再到 j
对于每一个k,我们都判断 d[i][j] 是否大于 d[i][k] + d[k][j],如果大于,就可以更新d[i][j]了。

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

三次循环完之后,遍历完所有的 k 后,d[i][j] 存的就是 i——>j的最短路了。


内容若有不对,还望大佬指正

最短路算法总结(超详细~)相关推荐

  1. 垃圾收集概述和垃圾收集算法(超详细介绍)

    文章目录 垃圾收集概述和垃圾收集算法(超详细介绍) 为什么我们还要去了解垃圾收集和内存分配 哪些内存需要回收 不需要回收的 需要回收的 方法区的回收 回收废弃常量 回收"不再被使用的类&qu ...

  2. 【算法】超详细的遗传算法(Genetic Algorithm)解析

    转自:https://www.jianshu.com/p/ae5157c26af9 [算法]超详细的遗传算法(Genetic Algorithm)解析 00 目录 遗传算法定义 生物学术语 问题导入 ...

  3. 陈宝林《最优化理论与算法》超详细学习笔记 (八)————最优性条件

    陈宝林<最优化理论与算法>超详细学习笔记 (八)----最优性条件 无约束问题的极值条件 必要条件 二阶充分条件 充要条件 约束极值问题的最优性条件 不等式约束的一阶最优性条件 无约束问题 ...

  4. 陈宝林《最优化理论与算法》超详细学习笔记 (七)————第五章 运输问题

    陈宝林<最优化理论与算法>超详细学习笔记 (七)----第五章 运输问题 第1节 运输问题的数学模型 第2节 表上作业法 2.1 确定初始基可行解 2.2 最优解的判别 2.3 改进的方法 ...

  5. 陈宝林《最优化理论与算法》超详细学习笔记 (四)————第四章 对偶理论

    陈宝林<最优化理论与算法>超详细学习笔记 (四)----第四章 对偶理论 1. 对偶问题的提出 2. 线性规划的对偶理论 2.1 原问题与对偶问题的关系 2.2 对偶问题的基本性质 3. ...

  6. 陈宝林《最优化理论与算法》超详细学习笔记 (一)————第十章 使用导数的最优化方法(最速下降法、牛顿法、阻尼牛顿法)

    陈宝林<最优化理论与算法>超详细学习笔记 (一)----第十章 使用导数的最优化方法(最速下降法.牛顿法.阻尼牛顿法) 写在前面 第十章 使用导数的最优化方法 最速下降法 牛顿法 阻尼牛顿 ...

  7. 陈宝林《最优化理论与算法》超详细学习笔记 (五)————最优性条件 之 KKT条件

    陈宝林<最优化理论与算法>超详细学习笔记 (五)----最优性条件 之 KKT条件 Lagrange对偶问题 原问题 Lagrange函数 Lagrange对偶函数 强/弱对偶性 弱对偶性 ...

  8. 陈宝林《最优化理论与算法》超详细学习笔记 (二)————补充知识(凸集) 第二章 线性规划的基本性质

    陈宝林<最优化理论与算法>超详细学习笔记 (二)----补充知识 凸集 & 第二章 线性规划的基本性质 补充知识 凸集 方向与极方向 表示定理 择一定理 第一章 线性规划的基本性质 ...

  9. 数据挖掘领域十大经典算法之—SVM算法(超详细附代码)

    相关文章: 数据挖掘领域十大经典算法之-C4.5算法(超详细附代码) 数据挖掘领域十大经典算法之-K-Means算法(超详细附代码) 数据挖掘领域十大经典算法之-Apriori算法 数据挖掘领域十大经 ...

  10. 数据挖掘领域十大经典算法之—AdaBoost算法(超详细附代码)

    相关文章: 数据挖掘领域十大经典算法之-C4.5算法(超详细附代码) 数据挖掘领域十大经典算法之-K-Means算法(超详细附代码) 数据挖掘领域十大经典算法之-SVM算法(超详细附代码) 数据挖掘领 ...

最新文章

  1. 【JavaScript总结】JavaScript语法基础:JS高级语法
  2. HTML5 canvas绘制雪花飘落
  3. KMeans中的K怎么选择?Elbow method怎么实施?
  4. Dos中@符号的使用演示(屏蔽echo off的回显)
  5. boost::phoenix::function相关的测试程序
  6. 语言是用的,把日常的交流转换成其它语言就可以了(转)
  7. Oracle分页排序采坑记录
  8. java 判断object类型_Java多态的6大特性|乐字节
  9. html工具箱源码,大灰狼的ASP工具箱——XMLHTTP的应用,获得远程的文件,获得远程HTML文件源码...
  10. 语音识别实验报告.docx
  11. hashmap原理_HashMap实现原理解读
  12. html5实现电子签名并下载
  13. 笔记本使用Nsight注意事项
  14. 企业公司Discuz模板
  15. oracle嵌套case when,case when 中是否可以在嵌套一个case when ?
  16. 用Wireshark简单分析HTTPS传输过程-抓包过程
  17. python配置(二)——机器学习环境
  18. 机器学习入门系列05,Classification: Probabilistic Generative Model(分类:概率生成模型)
  19. 专业系统开发流程有多少步骤?如何进行系统开发?
  20. J-Flash 读取Flash数据

热门文章

  1. 同一个二维码支持多种支付的实现思路
  2. 0基础,如何快速学习自媒体,详细教程
  3. 个人游戏开发者是如何盈利
  4. 施耐德变频器与昆仑通态触摸屏Modbus通讯程序实现正转反转,启停复位,频率设定等功能
  5. 做电脑技术员几年的心得
  6. android面板驱动的使用方法,高通平台Android 驱动层LCD显示屏驱动移植说明和相关工具...
  7. Android NDK开发1——开发流程+依赖外部so+生成自实现so+静态注册JNI+动态注册JNI
  8. linux微信电脑版登录不了,默认Windows与Deepin系统下的微信电脑版目录
  9. 【工具】IDEA打包jar包
  10. idea无法找到主启动类_IDEA打包jar-解决找不到或无法加载主类 main的问题