关于最短路径问题(图论)
模版题为【hdu 2544】最短路。比较详细的解释请见:【转】彻底弄懂最短路径问题(图论)
前言:我先写一些总结性的话——
1.推荐使用优先队列优化后的Dijkstra算法,速度快又稳定,而SPFA算法虽快但不稳定;但也有特殊情况,譬如说:【uva 658】It's not a Bug, it's a Feature!(图论--Dijkstra或spfa算法+二进制表示+类“隐式图搜索”)这些类“隐式图搜索”的题。然而......似乎我们这边大家都喜欢用SPFA,因为最初接触的就是它而且它就是一个BFS,比较好打;
2.出现负边和判断负环都用Bellman-Ford算法(也就是SPFA算法);
3.Floyd算法本质是DP或贪心思想,枚举出了所有路径的情况,一些“合法性”“可到达性”的题目可以用它。
1.Dijkstra算法
概述:依次将当前没有标记过的,且与源点的距离最近的点标记 / 纳入联盟内,并拓展该点,更新与之相连边的所有另一点的离源点的距离。 P.S.之前我以为几乎就是MST中的Prim算法;这个原理我真心不怎么理解 _(눈_ 눈」∠)_
实现:邻接表+优先队列。
时间复杂度:O(n^2+m); O(n log n+m)。
应用:有向图和无向图,正权图上的单源最短路(Single-Source Shortest Paths, SSSP,即从单个源点出发到所有结点的最短路)。
注意——正权图上可能有零环和正环,但都不会影响最短路的计算;Dijkstra中优先队列的top()是汇点的值时就可以跳出,因为剩下的可以拓展的点都比它的值大,由于没有负权边,就不可能通过一些边又比它小了。
1 #include<cstdio> 2 #include<cstdlib> 3 #include<cstring> 4 #include<iostream> 5 using namespace std; 6 7 const int N=110,M=10010,D=1010,INF=(int)1e9; 8 int n,m; 9 int last[N],d[N],vis[N]; 10 struct edge{int x,y,d,next;}a[2*M]; 11 12 int mmin(int x,int y) {return x<y?x:y;} 13 void ins(int len,int x,int y,int d) 14 { 15 a[len].x=x,a[len].y=y,a[len].d=d; 16 a[len].next=last[x],last[x]=len; 17 } 18 int Dijkstra() 19 { 20 memset(vis,0,sizeof(vis)); 21 memset(d,63,sizeof(d)); 22 d[1]=0; 23 for (int k=1;k<=n;k++)//加n个点进联盟 24 { 25 int p=0,mn=INF; 26 for (int i=1;i<=n;i++) 27 if (!vis[i] && d[i]<mn) p=i,mn=d[i]; 28 vis[p]=1; 29 for (int i=last[p];i;i=a[i].next) 30 { 31 int y=a[i].y; 32 d[y]=mmin(d[y],d[p]+a[i].d); 33 } 34 } 35 return d[n]; 36 } 37 int main() 38 { 39 while (1) 40 { 41 scanf("%d%d",&n,&m); 42 if (!n && !m) break; 43 memset(last,0,sizeof(last)); 44 for (int i=1;i<=m;i++) 45 { 46 int x,y,d; 47 scanf("%d%d%d",&x,&y,&d); 48 ins(2*i-1,x,y,d),ins(2*i,y,x,d); 49 } 50 printf("%d\n",Dijkstra()); 51 } 52 return 0; 53 } 54 55 Dijkstra
1 #include<cstdio> 2 #include<cstdlib> 3 #include<cstring> 4 #include<iostream> 5 #include<queue> 6 using namespace std; 7 8 const int N=110,M=10010,D=1010,INF=(int)1e9; 9 int n,m; 10 int last[N],vis[N],d[N];//,path[N]; 11 struct edge{int x,y,d,next;}a[2*M]; 12 struct node 13 { 14 int x,d; 15 bool operator < (const node& now) const 16 { return d>now.d; }// > 使原来的优先队列排序反转,dmin first out 17 node() {} 18 node(int u,int v) {x=u;d=v;} 19 }; 20 priority_queue<node> q;//默认从大到小排序 21 22 int mmin(int x,int y) {return x<y?x:y;} 23 void ins(int len,int x,int y,int d) 24 { 25 a[len].x=x,a[len].y=y,a[len].d=d; 26 a[len].next=last[x],last[x]=len; 27 } 28 int Dijkstra()//!!!important!! 29 { 30 while (!q.empty()) q.pop(); 31 memset(vis,0,sizeof(vis)); 32 memset(d,63,sizeof(d)); 33 d[1]=0; 34 q.push(node(1,0)); //这里无 vis[x]=1; 35 while (!q.empty()) 36 { 37 node now=q.top(); q.pop(); 38 int x=now.x,tmp=now.d; 39 if (x==n) return d[x];//见“注意” 40 if (vis[x]) continue;//或 if (tmp!=d[x]) continue; 表示标记过了,不再用它更新相关的点,以防止重复拓展 41 vis[x]=1; 42 for (int i=last[x];i;i=a[i].next) 43 { 44 int y=a[i].y; 45 if (d[x]+a[i].d<d[y]) 46 { 47 d[y]=d[x]+a[i].d; 48 //path[y]=x; 49 q.push(node(y,d[y]));//no judge 50 } 51 } 52 } 53 return -1; 54 } 55 int main() 56 { 57 while (1) 58 { 59 scanf("%d%d",&n,&m); 60 if (!n && !m) break; 61 memset(last,0,sizeof(last)); 62 for (int i=1;i<=m;i++) 63 { 64 int x,y,d; 65 scanf("%d%d%d",&x,&y,&d); 66 ins(2*i-1,x,y,d),ins(2*i,y,x,d); 67 } 68 printf("%d\n",Dijkstra()); 69 } 70 return 0; 71 } 72 73 Dijkstra+优先队列(important!!)
附上一个小笑话 ヾ(o◕∀◕)ノヾ Dijkstra这么厉害,为什么没有把求多点之间的最短路的Floyd算法也给发明了。——原因是,它叫Dijkstra,不叫Dkijstra。··(≧∀≦)··
2.Bellman-ford算法
概述:由于最多只包含n-1个结点,便通过n-1轮,对m条边进行松弛,更新端点的值。
实现:队列。
时间复杂度:O(nm)。
应用:有向图和无向图,正权和负权图的最短路,判断负环;若要算出单源最短路,需要让源点和每个点之间加一条权值为0的边,这样每个点都可以被访问。
1 #include<cstdio> 2 #include<cstdlib> 3 #include<cstring> 4 #include<iostream> 5 #include<queue> 6 using namespace std; 7 8 const int N=110,M=10010,D=1010,INF=(int)1e9; 9 int n,m; 10 int last[N],vis[N],d[N];//,path[N]; 11 struct edge{int x,y,d,next;}a[2*M]; 12 13 int mmin(int x,int y) {return x<y?x:y;} 14 void ins(int len,int x,int y,int d) 15 { 16 a[len].x=x,a[len].y=y,a[len].d=d; 17 a[len].next=last[x],last[x]=len; 18 } 19 int Bellman_ford()//!!!important!! 20 { 21 memset(vis,0,sizeof(vis)); 22 memset(d,63,sizeof(d)); 23 d[1]=0; 24 for (int k=1;k<n;k++) 25 for (int i=1;i<=m;i++) 26 { 27 int x=a[i].x,y=a[i].y; 28 d[y]=mmin(d[y],d[x]+a[i].d); 29 } 30 return d[n]; 31 } 32 int main() 33 { 34 while (1) 35 { 36 scanf("%d%d",&n,&m); 37 if (!n && !m) break; 38 memset(last,0,sizeof(last)); 39 for (int i=1;i<=m;i++) 40 { 41 int x,y,d; 42 scanf("%d%d%d",&x,&y,&d); 43 ins(2*i-1,x,y,d),ins(2*i,y,x,d); 44 } 45 m=2*m; 46 printf("%d\n",Bellman_ford()); 47 } 48 return 0; 49 }
3.SPFA算法(Shortest Path Faster Algorithm)
概述:如它的名字,它其实是在Bellman-ford算法的基础上加上一个队列优化,减少了冗余的松弛操作。
实现:队列。
时间复杂度:O(km),k为每个节点进入队列的次数,且k一般<=2;最坏情况是O(nm),比如图为完全图时会有n-1条边的松弛路径,每个点会入队n-1次。{详见Dijkstra、Bellman_Ford、SPFA、Floyd算法复杂度比较。}
应用:有向图和无向图,正权和负权图上的单源最短路,判断负环。拓展:若要算出单源最短路,需要让源点和每个点之间加一条权值为0的边,这样每个点都可以被访问。
1 #include<cstdio> 2 #include<cstdlib> 3 #include<cstring> 4 #include<iostream> 5 #include<queue> 6 using namespace std; 7 8 const int N=110,M=10010,D=1010,INF=(int)1e9; 9 int n,m; 10 int last[N],inq[N],cnt[N],d[N];//,path[N]; 11 struct edge{int x,y,d,next;}a[2*M]; 12 queue<int> q; 13 14 int mmin(int x,int y) {return x<y?x:y;} 15 void ins(int len,int x,int y,int d) 16 { 17 a[len].x=x,a[len].y=y,a[len].d=d; 18 a[len].next=last[x],last[x]=len; 19 } 20 int spfa() 21 { 22 while (!q.empty()) q.pop(); 23 memset(d,63,sizeof(d)); 24 memset(inq,0,sizeof(inq)); 25 memset(cnt,0,sizeof(cnt)); 26 q.push(1); 27 inq[1]=cnt[1]=1,d[1]=0; 28 while (!q.empty()) 29 { 30 int x=q.front(); 31 q.pop(), inq[x]=0; 32 for (int i=last[x];i;i=a[i].next) 33 { 34 int y=a[i].y; 35 if (d[x]+a[i].d<d[y]) 36 { 37 d[y]=d[x]+a[i].d; 38 //path[y]=x; 或 path[y]=i; 39 if (!inq[y]) 40 { 41 inq[y]=1,cnt[y]++; 42 if (cnt[y]>n) return -1;//> 判断负圈,一个点被更新了超过n次 43 q.push(y); 44 } 45 } 46 } 47 } 48 return d[n]; 49 } 50 int main() 51 { 52 while (1) 53 { 54 scanf("%d%d",&n,&m); 55 if (!n && !m) break; 56 memset(last,0,sizeof(last)); 57 for (int i=1;i<=m;i++) 58 { 59 int x,y,d; 60 scanf("%d%d%d",&x,&y,&d); 61 ins(2*i-1,x,y,d),ins(2*i,y,x,d); 62 } 63 printf("%d\n",spfa()); 64 } 65 return 0; 66 }
唉,其实用 bfs 的 spfa 判断负环很慢!应该用 dfs 的。具体解释和代码请见:【洛谷 P3385】模板-负环(图论--spfa)
4.Floyd-warshall算法
概述:枚举所有情况,限制每两点中间通过的结点范围而松弛n轮。
实现:DP思想,递推。
时间复杂度:O(n^3)。
应用:有向图和无向图,正权和负权图上的每两点之间的最短路问题 和 连通性结果为有向图的传递闭包(Transitive Closure. 数学上定义为:在集合X上的二元关系R的传递闭包是包含R的X上的最小传递关系)问题。
1 #include<cstdio> 2 #include<cstdlib> 3 #include<cstring> 4 #include<iostream> 5 #include<queue> 6 using namespace std; 7 8 const int N=110,M=10010,D=1010,INF=(int)1e9; 9 int n,m; 10 int d[N][N]; 11 12 int mmin(int x,int y) {return x<y?x:y;} 13 int Floyd() 14 { 15 for (int k=1;k<=n;k++) 16 for (int i=1;i<=n;i++) 17 for (int j=1;j<=n;j++)//此时的d[i][j]表示i,j间只能经过1~k点时的最短路径 18 d[i][j]=mmin(d[i][j],d[i][k]+d[k][j]);//原3维方程:d[k][i][j]=mmin(d[k][i][j],d[k-1][i][k]+d[k-1][k][j]); 19 return d[1][n]; 20 } 21 int main() 22 { 23 while (1) 24 { 25 scanf("%d%d",&n,&m); 26 if (!n && !m) break; 27 memset(d,63,sizeof(d)); 28 for (int i=1;i<=m;i++) 29 { 30 int x,y,t; 31 scanf("%d%d%d",&x,&y,&t); 32 d[x][y]=d[y][x]=t; 33 } 34 printf("%d\n",Floyd()); 35 } 36 return 0; 37 }