单源最短路Dijkstra+Bellman-Ford+spfa算法
单源最短路径
单源最短路径问题:给定一张有向图 G=(V,E),V是点集,E是边集,|V|=n,|E|=m,节点以[1,n]之间的连续整数编号,(u,v,w) 描述一条从u出发,到达v,长度为w的有向边。设s号节点为起点,求长度为n的数组d,其中d[i]表示从起点s到节点i的最短路径的长度。
Dijkstra算法
1.算法思想:Dijkstra算法基于贪心的思想,每次找到离起点s最近的一个顶点,然后以该顶点为中心进行扩展,最终得到源点到其余所有点的最短路径。
2.算法流程:
(1)初始化d[s]=0,其余节点的d值为正无穷大。
(2)找出未被标记,且d[u]最小的节点u,然后标记节点u。
(3)枚举u的所有出边(u,v,w),若d[v]>d[u]+w,则d[v]=d[u]+w。
(4)重复(2)(3)两个步骤,直到所有节点都被标记。
3.注意事项:Dijkstra算法只适用于所有边长为非负数的图,不适用于有负边权的图。当边长w都是非负数时,全局最小值不可能再被其他节点更新,所以在第(2)步选出的节点必然满足:d[u]已经是起点到u的最短路经。我们不断选择全局最小值进行标记和更新,最终可得到起点s到每个节点的最短路经长度。
4.例题:洛谷 P3371 https://www.luogu.com.cn/problem/P3371
AC代码如下:
1 #include<bits/stdc++.h>
2 using namespace std;
3 const int N=1e4+5,inf=2147483647;
4 int n,m,s,vis[N];
5 long long d[N];
6 vector<pair<int,int> >g[N];
7 queue<int>q;
8 void spfa()
9 {
10 fill(d,d+N,inf);
11 d[s]=0;
12 q.push(s);
13 vis[s]=1;
14 while(q.size())
15 {
16 int u=q.front();
17 q.pop();
18 vis[u]=0;
19 for(int i=0;i<g[u].size();i++)
20 {
21 int v=g[u][i].first,w=g[u][i].second;
22 if(d[v]>d[u]+w)
23 {
24 d[v]=d[u]+w;
25 if(vis[v]==0)
26 {
27 q.push(v);
28 vis[v]=1;
29 }
30 }
31 }
32 }
33 }
34 int main()
35 {
36 cin>>n>>m>>s;
37 int x,y,w;
38 for(int i=1;i<=m;i++) cin>>x>>y>>w,g[x].push_back(make_pair(y,w));
39 spfa();
40 for(int i=1;i<=n;i++) cout<<d[i]<<" ";
41 return 0;
42 }
4.优化:该程序的时间复杂度为O(n2),主要问题在第(2)步寻找全局最小值的过程。可用优先队列(priority_queue)对d数组进行维护,用O(log n)的时间获取最小值并从堆中删除,用O(log n)的时间执行一条边的扩展与更新,最终可在O(m log n)的时间内实现Dijkstra算法。(严格来说,时间复杂度为((m+n)log n),假设m的规模不小于n的规模简写为(m log n)。
例题:洛谷P4779 https://www.luogu.com.cn/problem/P4779
该题若不优化,则全TLE,AC代码如下:
1 #include<bits/stdc++.h>
2 #define pa pair<int,int>
3 using namespace std;
4 const int N=1e5+5,inf=2147483647;
5 int n,m,s,vis[N];
6 long long d[N];
7 vector<pa>g[N];
8 priority_queue<pa,vector<pa>,greater<pa> >q;//堆优化
9 void dijkstra()
10 {
11 fill(d,d+N,inf);
12 d[s]=0;
13 q.push(make_pair(0,s));
14 //初始化,pair以第一关键字进行排序,因此pair的第一关键字为d距离,第二关键字为节点编号
15 while(q.size())
16 {
17 pa t=q.top();
18 q.pop();
19 int u=t.second;
20 if(vis[u]) continue;
21 vis[u]=1;
22 for(int i=0;i<g[u].size();i++)
23 {
24 int v=g[u][i].first,w=g[u][i].second;
25 if(d[v]>d[u]+w)
26 {
27 d[v]=d[u]+w;
28 q.push(make_pair(d[v],v));//更新,把新的二元组插入堆
29 }
30 }
31 }
32 }
33 int main()
34 {
35 cin>>n>>m>>s;
36 int x,y,w;
37 for(int i=1;i<=m;i++) cin>>x>>y>>w,g[x].push_back(make_pair(y,w));
38 dijkstra();
39 for(int i=1;i<=n;i++) cout<<d[i]<<" ";
40 return 0;
41 }
Bellman-Ford算法
给定一张有向图,若对于图中的每一条边(u,v,w),都有d[v]<=d[u]+w成立,则称该边满足三角不等式。若所有边满足三角不等式,则d数组就是所求最短路。
1.算法思想:迭代求解:反复对边集E中的每条边进行松弛操作,使得顶点集V中的每个顶点v的最短距离估计值逐步逼近其最短距离。(运行n-1次)
松弛操作:每次松弛操作实际上是对相邻节点的访问(相当于广度优先搜索),第i次松弛操作保证了所有深度为i的路径最短。由于图的最短路径最长不会经过超过n - 1条边,所以可知Bellman-Ford算法所得为最短路径,也可知时间复杂度为O(mn)。
证明:
图的任意一条最短路经既不能包含负权回路,也不会包含正权回路,因此它最多包含n-1条边。
其次,从源点s可达的所有顶点如果 存在最短路径,则这些最短路径构成一个以s为根的最短路径树。Bellman-Ford算法的迭代松弛操作,实际上就是按每个点实际的最短路径(虽然我们还不知道,但它一定存在)的层次,逐层生成这棵最短路径树的过程。
2.算法流程:
(1)初始化d[s]=0,其余节点的d值为正无穷大。
(2)松弛操作:进行n-1次遍历,对于图中的每条边:如果d[u]+w<d[v],则d[v]=d[u]+w。
(3)判断负环:遍历图中的所有边,计算 u 至 v 的距离,如果对于 v 存在更小的距离,则说明存在环。
3.注意事项:Bellman-Ford算法适用于有负边权的图,也可以判断负环。即当进行第n次操作时,计算 u 至 v 的距离,如果对于 v 存在更小的距离,则说明存在环。
4.优化:Bellman-Ford算法时间复杂度过高,因此通常采用队列优化,即SPFA算法。
SPFA算法
1.算法思想:Bellman-Ford算法的优化。
2.算法流程:
(1)初始化d[s]=0,其余节点的d值为正无穷大,建立一个队列,将起点s入队。
(2)取出队头节点u,枚举u的所有出边(u,v,w),若d[v]>d[u]+w,则d[v]=d[u]+w。同时,若v不在队列中,把v入队。
(3)重复(2)步骤,直至队列为空。
3.注意事项:如果某个点进入队列的次数超过N次则存在负环(判断负环),此时跳出循环。
在任意时刻,该算法的队列都保存了待扩展的节点。每次入队相当于完成一次d数组的更新操作,使其满足三角不等式。一个节点可能会入队、出队多次。最终,图中节点收敛到全部满足三角板不等式的状态。这个队列避免了Bellman-Ford算法中对不需要扩展的节点的冗余扫描,在随机图上运行的效率为O(km)级别,其中k是一个较小的常数。但在特殊构造的图上,该算法很可能退化为O(nm),必须谨慎使用。
例题:洛谷P3371 https://www.luogu.com.cn/problem/P3371
AC 代码如下:
1 #include<bits/stdc++.h>
2 using namespace std;
3 const int N=1e4+5,inf=2147483647;
4 int n,m,s,vis[N];
5 long long d[N];
6 vector<pair<int,int> >g[N];
7 queue<int>q;
8 void spfa()
9 {
10 fill(d,d+N,inf);
11 d[s]=0;
12 q.push(s);
13 vis[s]=1;//初始化,若节点入队则标记
14 while(q.size())
15 {
16 int u=q.front();
17 q.pop();
18 vis[u]=0;//出队,标记清空
19 for(int i=0;i<g[u].size();i++)
20 {
21 int v=g[u][i].first,w=g[u][i].second;
22 if(d[v]>d[u]+w)
23 {
24 d[v]=d[u]+w;//更新
25 if(vis[v]==0)
26 {
27 q.push(v);
28 vis[v]=1;//若v不在队列里,入队,标记
29 }
30 }//松弛操作
31 }
32 }
33 }
34 int main()
35 {
36 cin>>n>>m>>s;
37 int x,y,w;
38 for(int i=1;i<=m;i++) cin>>x>>y>>w,g[x].push_back(make_pair(y,w));
39 spfa();
40 for(int i=1;i<=n;i++) cout<<d[i]<<" ";
41 return 0;
42 }
最短路题目:
洛谷P1144 https://www.luogu.com.cn/problem/P1144
洛谷P1462 https://www.luogu.com.cn/problem/P1462
洛谷P5304 https://www.luogu.com.cn/problem/P5304
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本