图论——最短路
No.1 引入
学最短路时,脑子还比较清醒,趁时候写篇博总结总结。
No.2 概念与分类
最短路问题旨在寻找两结点之间的最短路径。
我们可根据只有一个起点与多个起点分为求单源最短路与多源最短路。
单源最短路:已知一个起点,求出从该起点出发到其余点的最短的路径的长度是多少。
多源最短路:已知一个图 ,求出所有点到所有点的最短的路径的长度。
No.3 Floyd算法
1)Floyd算法是什么?
Floyed算法是一种利用动态规划的思想寻找给定的加权图中多源点之间最短路径的算法。该算法名称以创始人之一、1978年图灵奖获得者、斯坦福大学计算机科学系教授罗伯特·弗洛伊德命名。
2)Floyd算法的思想
动态规划(dp),递推产生一个矩阵序列DP1,DP2,.....,DPk,...,DPn(图有n个节点),DPk=(ak(i,j))nxn。其中矩阵DPk第i行第j列表示从顶点vi到顶点vj的路径上经过的顶点序号不大于k的最短路径长度。
也就是三个循环嵌套,在循环之中不断比较从i到j的最短距离与从i到k最短距离加上从k到j最短距离的大小,如果经过这个点,路径变短了,我们就接受这个点,认为可以经过这个点;否则就不经过这个点,就是从i到j最短。
状态转移方程就为:
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j])
3)Floyd算法需注意的点
需注意的是,Floyd算法最外层循环需从小到大枚举点k,because点k是DP的阶段循环,如果放在里面,就会出现用未知去求未知的情况。
4)举栗
如图,我们要求图中所有点的最短路,就先以1点刷新所有距离。在依次枚举k点,更新答案。最后,dp[i][j]就为i~j的最短路。dp[1][4]就等于4。
5)Floyd算法时间复杂度分析
由于我们使用的是三重循环,每次都为1~n,所以时间复杂度就约为O(n3)。
6)核心代码
Code
void Floyd(){
for(int k=1;k<=n;k++){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(flo[i][j]>flo[i][k]+flo[k][j]){
flo[i][j]=flo[i][k]+flo[k][j];
}
}
}
}
}
7)Floyd算法总结
优点:Floyd算法想起来不算复杂,实现起来更是简单,容易上手,并且因不用确认起点,每一点都会遍历,它可以轻松实现多源最短路。
缺点:十分明显,时间复杂度高。
No.4 Dijkstra算法
1)Dijkstra算法是什么?
其实Dijkstra(迪科斯彻)是一个人,被称为结构程序设计之父,在1972年获得图灵奖(计算机界诺⻉尔奖)。
2)Dijkstra算法的思想
本质是贪心,首先假定源点为u,顶点集合V被划分为两部分:集合 S 和 V-S。初始时S中仅含有源点u,其中S中的顶点到源点的最短路径已经确定。
集合S 和V-S中所包含的顶点到源点的最短路径的长度待定,用dis[]记录当前每个顶点对应的最短特殊路径长度。
3)Dijkstra算法需注意的点
Dijkstra算法只能应用于正权图,因为当把一个节点选入集合S时,即意味着已经找到了从源点到这个点的最短路径,但若存在负权边,就与这个前提矛盾,可能会出现得出的距离加上负权后比已经得到S中的最短路径还短。
4)Dijkstra算法的优化
我们可以定义一个优先队列,队列中元素记录了节点的编号和节点的最短路径值,将源点压入队列。当队列非空,执行以下操作:
1.u等于队顶的节点,w等于队顶节点的最短路径值
2.遍历u的所有边,如果能找到节点v最短路径值小于v的当前值,更新v,将v压入队列。
这样就减少了选择全局最小值的时间。
5)Dijkstra算法时间复杂度分析
如未优化,直接在集合中暴力寻找最短路长度最小的结点,两重循环,时间约为O(n2)。
使用优先队列维护时,时间就变成了O(mlogm)。
6)核心代码
Code
void Dijkstra(){
memset(dis,127,sizeof(dis));
dis[s]=0;
q.push(Edge{0,s});
while(q.size()){
Edge t=q.top();
q.pop();
int x=t.no;
if(vis[x]){
continue;
}
vis[x]=1;
for(int i=head[x];i;i=e[i].next){
int v=e[i].v;
if (dis[v]>dis[x]+e[i].w){
dis[v]=dis[x]+e[i].w;
q.push(Edge{dis[v],v});
}
}
}
}
7)Dijkstra算法总结
优点:Dijkstra算法思想上易理解,时间复杂度较Floyd算法大大降低。
缺点:只能应用于单源图,不能出现负权,且代码较Floyd算法比较难打。
No.5 Bellman-Ford算法
1)Bellman-Ford算法是什么?
Bellman-Ford是一种算法,来求解最短路径。该算法由美国数学家理查德•贝尔曼(Richard Bellman, 动态规划的提出者)和小莱斯特•福特(Lester Ford)发明。
2)Bellman-Ford算法的思想
1. 初始化源点s到各个点v的路径dis[v]=∞,dis[s]=0。
2. 进行n-1次遍历,每次遍历对所有边进行松弛操作,满足则将权值更新。
松弛操作:以a为起点,b为终点,ab边长度为w为例。dis[a]代表源点s到a点的路径长度,dis[b]代表源点s到b点的路径长度。如果满足dis[b]>dis[a]+w则将dis[b]更新为dis[a]+w。
3. 遍历都结束后,若再进行一次遍历,还能得到s到某些节点更短的路径的话,则说明存在负环路。
3)Bellman-Ford算法需注意的点
(1)为何对每条边进行n-1次松弛后能够确保正确?
最坏的情况下,从源结点到一个结点的最短路径需要经过所有结点、所有边,即每条边的最坏情况是需要n-1次松弛。
(2)为何dis[b]>dis[a]+w时存在环路?
因为通过(1)我们知道在经过n-1次松弛之后能确保v正确,那么dis[b]应该<=dis[a]+w,不可能出现dis[b]>dis[a]+w的情况,如果出现只有可能存在环路,每次松弛都会使dis无限缩小。
(3)为何只能用于有向负权图?
因为如是无向负权图,负边权边的节点就会先后变化,后变化的节点会受影响,是错误的。
4)Bellman-Ford算法时间复杂度分析
Bellman-Ford对每条边进行n-1次松弛,两重循环,时间约为O(nm)。
5)核心代码
Code
void Bellman_Ford(){
memset(dis,0x3f,sizeof(dis));
memset(pre,0,sizeof(pre));
dis[s]=0;
for(int i=1;i<=n-1;i++){
for(int j=1;j<=m;j++){
if(dis[v[j]]>dis[u[j]]+w[j]){
pre[v[j]]=u[j];
dis[v[j]]=dis[u[j]]+w[j];
}
else if(dis[v[j]]==dis[u[j]]+w[j]){
pre[v[j]]=min(pre[v[j]],u[j]);
}
}
}
bool f=0;
for(int i=1;i<=m;i++){
if(dis[u[i]]+w[i]<dis[v[i]]){
f=1;
}
}
}
6)Bellman-Ford算法总结
优点:易理解,可用于有向负权图。
缺点:慢。
No.6 SPFA算法
1)SPFA算法是什么?
SPFA(Shortest Path Faster Algorithm)算法是西南交通大学的段凡丁于1994提出,是队列优化版的Bellman-Ford。
2)SPFA算法的思想
将源点加入队列;每次从队列出来一个点,对相邻的点,进行松弛操作;被松弛成功且不在队列的点依次进入队列;重复操作,直至队列为空算法结束。
3)SPFA算法需注意的点
为什么SPFA可以处理有向负边:
和Bellman-Ford一样,因为在SPFA中每一个点松弛过后说明这个点距离更近了,所以有可能通过这个点会再次优化其他点,所以将这个点入队再判断一次,而Dijkstra中是贪心的策略,每个点选择之后就不再更新,如果碰到了负边的存在就会破坏这个贪心的策略就无法处理了。
由于SPFA算法是由Bellman_ford算法优化而来,在最坏的情况下时间复杂度和它一样即时间复杂度为 O(nm) ,假如题目时间允许可以直接用SPFA算法去解Dijkstra算法的题目。
4)核心代码
Code
void SPFA(){
memset(dis,0x3f,sizeof(dis));
dis[s]=0;
vis[s]=1;
q.push(s);
while(!q.empty()){
int u=q.front();
q.pop();
vis[u]=0;
for(int i=0;i<Graph[u].size();i++){
int v=Graph[u][i].v;
int w=Graph[u][i].w;
if(dis[v]>dis[u]+w){
dis[v]=dis[u]+w;
cnt[v]=cnt[u]+1;
pre[v]=u;
if(cnt[v]==n){
printf("No Solution");
exit(0);
}
if(!vis[v]){
vis[v]=1;
q.push(v);
}
}
else if(dis[v]==dis[u]+w){
pre[v]=min(pre[v],u);
}
}
}
}
5)SPFA算法总结
优点:易理解,可用于有向负权图,时间上较Bellman-Ford快。
缺点:在最坏的情况下时间复杂度和Bellman-Ford一样即时间复杂度为 O(nm) ,且v容易被卡。
No.7 总结
求最短路算法很多,各有各的好处,可根据题意分别使用。
最主要的还是多练习,多巩固,才能提高。
完结!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!