单源最短路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

 

posted @   Snoww  阅读(72)  评论(2编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
点击右上角即可分享
微信分享提示