企图掌握最短路基础
稠密图用邻接矩阵,稀疏图用邻接表
判断方法:边数m是点n的n^2级别的话就是稠密图,m是n级别的就是稀疏图
一:单源最短路——所有边权均为正数
1.朴素dijkstra算法O(n^2):
适用于单源最短路所有边权均为正数(可以有环,但是不能有负权边)的稠密图(点数n很小,边数m很大)
思路:
集合S为已经确定最短路径的点集。
1. 初始化距离
一号结点的距离为零,其他结点的距离设为无穷大(看具体的题)。
2. 循环n次,每一次将集合S之外距离最短X的点加入到S中去(这里的距离最短指的是距离1号点最近。点X的路径一定最短,基于贪心)。
然后用点X更新X邻接点的距离。
1 #include <iostream>
2 #include <cstring>
3 #include <algorithm>
4 using namespace std;
5
6 int n,m;
7 const int N=510;
8 int dist[N]; //dist[i]表示从1走到i的目前最短距离
9 int g[N][N]; //g[a][b]表示有向边a-->b的权重
10 bool st[N]; //记录是否找到了源点到该节点的最短距离
11
12 int dijkstra()
13 {
14 memset(dist,0x3f,sizeof dist);
15 dist[1]=0; //将点1的距离为0,其余均为正无穷
16
17 for(int i=2;i<=n;i++) //点1已经确定最短路距离为0,不需要下面的步骤
18 {
19 int t=-1; //t表示在还未确定最短路的点中找距离最近的点
20
21 for(int j=1;j<=n;j++)//在还未确定最短路的点中找距离最近的点
22 {
23 if(!st[j]&&(t==-1||dist[t]>dist[j])) t=j;
24 }
25
26 for(int k=1;k<=n;k++) //通过更改的t来进一步求其他点的最短路
27 {
28 dist[k]=min(dist[k],dist[t]+g[t][k]);
29 }
30
31 st[t]=1;
32
33 }
34
35 if(dist[n]==0x3f3f3f3f)return -1;
36 else return dist[n];
37
38 }
39
40 int main()
41 {
42 scanf("%d%d",&n,&m);
43
44 memset(g,0x3f,sizeof g);
45 while(m--)
46 {
47 int x,y,z;
48 scanf("%d%d%d",&x,&y,&z);
49 g[x][y]=min(g[x][y],z); //存在重边,取最短的距离
50 }
51
52
53 printf("%d",dijkstra());
54
55
56
57 return 0;
58 }
2.堆优化版dijkstra算法O(mlogn):
适用于单源最短路所有边权均为正数(可以有环,但是不能有负权边)的稀疏图
思路:
堆优化版的dijkstra是对朴素版dijkstra进行了优化,在朴素版dijkstra中时间复杂度最高的寻找距离最短的点O(n^2)可以使用最小堆优化。
1. 一号点的距离初始化为零,其他点初始化成无穷大。
2. 将一号点放入堆中。
3. 不断循环,直到堆空。每一次循环中执行的操作为:
弹出堆顶(与朴素版diijkstra找到S外距离最短的点相同,并标记该点的最短路径已经确定)。
用该点更新临界点的距离,若更新成功就加入到堆中。
1 #include <bits/stdc++.h> 2 using namespace std; 3 int n,m,s,idx; 4 const int N=2e5+100; 5 int h[N],dist[N]; 6 bool st[N]; 7 8 typedef pair<int,int>pii; 9 priority_queue<pii,vector<pii>,greater<pii> >q; 10 11 struct EDG 12 { 13 int ne,to,w; 14 }e[N]; 15 16 void add(int x,int y,int v) //ne指x,to指y 17 { 18 idx++; 19 e[idx].ne=h[x];e[idx].to=y;e[idx].w=v;h[x]=idx; 20 } 21 void dijkstra() 22 { 23 memset(dist,0x3f,sizeof dist); 24 q.push({0,s}); //1为到源点的距离,2为该点的编号 25 dist[s]=0; 26 while(q.size()) 27 { 28 pii sta=q.top();q.pop(); 29 int x=sta.second,distance=sta.first; 30 if(st[x]==1)continue; 31 st[x]=1; 32 for(int i=h[x];i;i=e[i].ne) 33 { 34 int y=e[i].to; 35 if(dist[y]>dist[x]+e[i].w) 36 { 37 dist[y]=dist[x]+e[i].w; 38 if(!st[y]) 39 q.push({dist[y],y}); 40 } 41 } 42 } 43 for(int i=1;i<=n;i++)printf("%d ",dist[i]); 44 return; 45 } 46 47 int main() 48 { 49 scanf("%d%d%d",&n,&m,&s); 50 for(int i=1;i<=m;i++) 51 { 52 int x,y,w; 53 scanf("%d%d%d",&x,&y,&w); 54 add(x,y,w); 55 } 56 57 dijkstra(); 58 59 60 return 0; 61 }
1 #include <iostream> 2 #include <cstring> 3 #include <queue> 4 using namespace std; 5 6 typedef pair<int,int> pii; 7 const int N=1e6+10; 8 9 int n,m; 10 int e[N],ne[N],h[N],idx,w[N]; 11 int dist[N]; 12 bool st[N]; 13 14 15 void add(int x,int y,int z) 16 { 17 e[idx]=y;ne[idx]=h[x];w[idx]=z;h[x]=idx++;19 //有重边也不要紧 20 //假设1->2有权重为2和3的边,再遍历到点1的时候2号点的距离会更新两次放入堆中 21 //这样堆中会有很多冗余的点 22 //但是在弹出的时候还是会弹出最小值2+x(x为之前确定的最短路径) 23 //标记st为1,所以下一次弹出3+x会continue不会向下执行。 24 } 25 26 int dijkstra() 27 { 28 memset(dist,0x3f,sizeof dist); 29 dist[1]=0; 30 priority_queue<pii,vector<pii>,greater<pii>> heap; 31 //first为距离,second为点的编号 32 heap.push({0,1}); 33 34 while(heap.size()) 35 { 36 auto t=heap.top(); 37 heap.pop(); 38 int ver = t.second, distance = t.first; 39 //ver表示在没有找到最短路的点中距离最近的点的编号 40 //distance表示1到这个点的距离 41 42 if(st[ver])continue; 43 //因为st数组就是判断这个点的最短路是否已经被确定了 44 //如果是true说明这个点已经确定了,没必要再进行下去了(重边情况) 45 46 47 st[ver]=1; 48 49 for(int i=h[ver];i!=-1;i=ne[i]) 50 { 51 int j=e[i]; //i只是个下标idx,e中在存的是i这个下标对应的点 52 if(dist[j]>distance+w[i]) 53 { 54 dist[j]=distance+w[i]; 55 heap.push({dist[j],j}); //把更新后的点放入堆中 56 } 57 58 } 59 } 60 61 if(dist[n]==0x3f3f3f3f)return -1; 62 return dist[n]; 63 } 64 65 int main() 66 { 67 scanf("%d%d",&n,&m); 68 69 memset(h,-1,sizeof h); 70 memset(w,0x3f,sizeof w); 71 while(m--) 72 { 73 int x,y,z; 74 scanf("%d%d%d",&x,&y,&z); 75 add(x,y,z); 76 } 77 78 printf("%d",dijkstra()); 79 80 81 return 0; 82 }
二:单源最短路——边权有负数存在
3.bellman-ford算法O(mn):
用处:当题中要求从 1 号点到 n 号点的最多经过 k 条边时,再用bellman_ford算法
1 #include <iostream>
2 #include <cstring>
3 using namespace std;
4
5 const int N=510,M=1e4+10;
6 int dist[N],last[N]; // dist[x]存储1到x的最短路距离
7 int n,m,k;
8
9 struct Edge
10 {
11 int a,b,w;
12 }edgs[M];
13
14 void bellman_ford()
15 {
16 memset(dist,0x3f,sizeof dist);
17 dist[1]=0;
18 //题中要求从 1 号点到 n 号点的最多经过 k 条边
19 //也正是因为这个要求,所以才要用bellman
20 for(int i=1;i<=k;i++)
21 {
22 memcpy(last,dist,sizeof dist);
23 //保留上一次迭代的dist的副本,防止跨层数更新
24 for(int j=0;j<m;j++)
25 {
26 auto e = edgs[j];
27 dist[e.b] = min(dist[e.b], last[e.a] + e.w);
28 }
29 }
30 }
31
32 int main()
33 {
34 scanf("%d%d%d",&n,&m,&k);
35
36 for(int i=0;i<m;i++)
37 {
38 int x,y,z;
39 scanf("%d%d%d",&x,&y,&z);
40 edgs[i]={x,y,z};
41 }
42
43 bellman_ford();
44
45 if(dist[n]>=0x3f3f3f3f/2)puts("impossible");
46 //因为有负边,可能最后是正无穷-2
47 else printf("%d\n",dist[n]);
48
49
50
51
52 return 0;
53 }
4.spfa算法平均情况下 O(m),最坏情况下 O(nm):
spfa不可存在负权回路,不可以求最多经过k条边
spfa是bellman的优化。Bellman_ford算法会遍历所有的边,但是有很多的边遍历了其实没有什么意义,我们只用遍历那些到源点距离变小的点所连接的边即可,只有当一个点的前驱结点更新了,该节点才会得到更新;因此考虑到这一点,我们将创建一个队列每一次加入距离被更新的结点。
spfa和dijkstra的区别:
1] Dijkstra算法中的st数组保存的是当前确定了到源点距离最小的点,且一旦确定了最小那么就不可逆了(不可标记为true后改变为false);SPFA算法中的st数组仅仅只是表示的当前发生过更新的点,且spfa中的st数组可逆(可以在标记为true之后又标记为false)。顺带一提的是BFS中的st数组记录的是当前已经被遍历过的点。
2] Dijkstra算法里使用的是优先队列保存的是当前未确定最小距离的点,目的是快速的取出当前到源点距离最小的点;SPFA算法中使用的是队列(你也可以使用别的数据结构),目的只是记录一下当前发生过更新的点。
bellman与spfa最后判断条件的区别:
Bellman_ford算法里最后的判断条件写的是dist[n]>0x3f3f3f3f/2;而spfa算法写的是dist[n]==0x3f3f3f3f;其原因在于Bellman_ford算法会遍历所有的边,因此不管是不是和源点连通的边它都会得到更新;但是SPFA算法不一样,它相当于采用了BFS,因此遍历到的结点都是与源点连通的,因此如果你要求的n和源点不连通,它不会得到更新,还是保持的0x3f3f3f3f。
spfa不可存在负权回路,bellman可以:
Bellman_ford算法可以存在负权回路,是因为其循环的次数是有限制的因此最终不会发生死循环;但是SPFA算法不可以,由于用了队列来存储,只要发生了更新就会不断的入队,因此假如有负权回路请你不要用SPFA否则会死循环。
求负环一般用spfa:
方法是用一个cnt数组记录每个点到源点的边数,一个点被更新一次就+1,一旦有点的边数达到了n那就证明存在了负环。
1 #include <iostream>
2 #include <queue>
3 #include <cstring>
4 using namespace std;
5
6 const int N=1e5+10;
7 int n,m;
8 int dist[N];
9 int h[N],ne[N],e[N],idx,w[N];
10 bool st[N];
11
12 void add(int x,int y,int z)
13 {
14 e[idx]=y;ne[idx]=h[x];w[idx]=z;h[x]=idx++;
15 }
16
17 void spfa()
18 {
19 queue<int>q;
20 memset(dist,0x3f,sizeof dist);
21 dist[1]=0;
22 q.push(1);
23 while(q.size())
24 {
25 int t=q.front();
26 q.pop();
27 st[t]=0; //代表之后该节点如果发生更新可再次入队
28 for(int i=h[t];i!=-1;i=ne[i])
29 {
30 int j=e[i];
31 if(dist[j]>dist[t]+w[i])
32 {
33 dist[j]=dist[t]+w[i];
34 if(!st[j]) //当前已经加入队列的结点,无需再次加入队列
35 //即便发生了更新也只用更新数值即可,重复添加降低效率
36 {
37 st[j]=1;
38 q.push(j);
39 }
40 }
41 }
42 }
43 }
44
45 int main()
46 {
47 scanf("%d%d",&n,&m);
48 memset(h,-1,sizeof h);
49 for(int i=1;i<=m;i++)
50 {
51 int x,y,z;
52 scanf("%d%d%d",&x,&y,&z);
53 add(x,y,z);
54 }
55
56 spfa();
57 if(dist[n]==0x3f3f3f3f)puts("impossible");
58 else printf("%d\n",dist[n]);
59
60
61
62
63 return 0;
64 }
判断负环的代码:
1 #include <iostream>
2 #include <queue>
3 #include <cstring>
4 using namespace std;
5
6 const int N=1e5+10;
7 int n,m;
8 int dist[N],cnt[N];
9 int h[N],ne[N],e[N],idx,w[N];
10 bool st[N];
11
12 void add(int x,int y,int z)
13 {
14 e[idx]=y;ne[idx]=h[x];w[idx]=z;h[x]=idx++;
15 }
16
17 bool spfa()
18 {
19 queue<int>q;
20 //不用初始化dist,如果存在负环,那么dist不管初始化为多少,都会被更新
21
22 for(int i=1;i<=n;i++)
23 //不仅仅是1了, 因为点1可能到不了有负环的点, 因此把所有点都加入队列
24 {
25 q.push(i);
26 st[i]=1;
27 }
28
29
30 while(q.size())
31 {
32 int t=q.front();
33 q.pop();
34 st[t]=0; //代表之后该节点如果发生更新可再次入队
35 for(int i=h[t];i!=-1;i=ne[i])
36 {
37 int j=e[i];
38 if(dist[j]>dist[t]+w[i])
39 {
40 dist[j]=dist[t]+w[i];
41 cnt[j]=cnt[t]+1;
42 if(cnt[j]>n)return 0;
43 if(!st[j]) //当前已经加入队列的结点,无需再次加入队列
44 //即便发生了更新也只用更新数值即可,重复添加降低效率
45 {
46 st[j]=1;
47 q.push(j);
48 }
49 }
50 }
51 }
52 return 1;
53 }
54
55 int main()
56 {
57 scanf("%d%d",&n,&m);
58 memset(h,-1,sizeof h);
59 for(int i=1;i<=m;i++)
60 {
61 int x,y,z;
62 scanf("%d%d%d",&x,&y,&z);
63 add(x,y,z);
64 }
65
66 if(spfa())puts("No");
67 else puts("Yes");
68
69
70
71
72 return 0;
73 }
三:多源汇最短路
5.floyd算法O(n^3):
多源汇最短路只能用floyd
1 #include <iostream>
2
3 using namespace std;
4
5 const int N=210,INF=1e9;
6 int n,m,k;
7 int d[N][N];
8
9 void floyd()
10 {
11 for(int c=1;c<=n;c++)
12 for(int i=1;i<=n;i++)
13 for(int j=1;j<=n;j++)
14 d[i][j]=min(d[i][j],d[i][c]+d[c][j]);
15
16
17 }
18
19 int main()
20 {
21 scanf("%d%d%d",&n,&m,&k);
22
23 for(int i=1;i<=n;i++)
24 for(int j=1;j<=n;j++)
25 {
26 if(i==j)d[i][j]=0;
27 else d[i][j]=INF;
28 }
29
30 for(int i=1;i<=m;i++)
31 {
32 int x,y,z;
33 scanf("%d%d%d",&x,&y,&z);
34 d[x][y]=min(d[x][y],z);
35 }
36
37 floyd();
38
39 while(k--)
40 {
41 int x,y;
42 scanf("%d%d",&x,&y);
43 if(d[x][y]>=INF/2)puts("impossible");
44 else printf("%d\n",d[x][y]);
45 }
46
47
48
49 }
最最最最后的总结:
1.Dijkstra-朴素 O(n2):
1.初始化距离数组, dist[1] = 0, dist[i] = inf;
2.for n次循环 每次循环确定一个min加入S集合中,n次之后就得出所有的最短距离
3.将不在S中dist_min的点->t
4.t->S加入最短路集合
5.用t更新到其他点的距离
2.Dijkstra-堆优化 O(mlogm):
1.利用邻接表,优先队列
2.在priority_queue<PII,vector<PII>,greater<PII>> heap;中将返回堆顶
3.利用堆顶来更新其他点,并加入堆中类似宽搜
3.Bellman_ford O(nm)O(nm)
1.注意连锁想象需要备份, struct Edge{int a,b,c} Edge[M];
2.初始化dist, 松弛 dist[x.b] = min(dist[x.b], backup[x.a]+x.w);
3.松弛k次,每次访问m条边
4.Spfa O(n)∼O(nm)O(n)∼O(nm)
1.利用队列优化仅加入修改过的地方
2.for k次
3.for 所有边利用宽搜模型去优化bellman_ford算法
4.更新队列中当前点的所有出边
5.Floyd O(n3)O(n3)
1.初始化d
2.k, i, j 去更新d