这是一道标准的模板题,所以拿来作为这一段时间学习最短路的总结题目。
题意很简单:
有多组输入数据,每组的第一行为两个整数n, m。表示共有n个节点,m条边。
接下来有m行,每行三个整数a, b, c。表示从a到b或从b到a长度为c。
求从1到n的最短路。
先说Floyd——
这个算法看上去就是一个三重for循环,然后在循环里不断对选择的两个节点进行松弛(感觉松弛这两个字很高端有没有)。
算法时间复杂度为O(n^3),n为节点数。所以一般可以用来处理规模1000以下的数据(即100数量级的,但是如果常数比较大的话也会超时,一般都是规模100的数据)。
但是,这是我目前掌握的唯一一个可以直接计算多源最短路的算法(即算法执行之后,获得图中任意两点间的最短路)。
代码如下:
1 void Floyd() 2 { 3 for(int k = 1; k <= n; k++) //选择的中间节点 4 for(int i = 1; i <= n; i++) //选择的源节点,即出发点 5 for(int j = 1; j <= n; j++) //选择的目标节点 6 if(mp[i][j] > mp[i][k] + mp[k][j]) 7 mp[i][j] = mp[i][k]+mp[k][j]; //如果经过中间节点的路径(即i到k到j)小于直接从i到j,则进行更新操作(松弛) 8 }
这道题用floyd跑了41ms。
接着是Dijkstra——
Dijkstra的时间复杂度笼统的说是O(n^2), 精确一点是O(n^2+m), 如果源点可达,那么是O(n*lg n+m*lg n) => (m*lg n)。n是点数,m是边数。一般来说,规模10^4的数据是可以在1s内完成的。
Dijkstra可以用来求单源最短路,也就是从一个点出发,到其他所有点的最短路。
注意,当边存在负权的时候,Dijkstra就会求出错误答案了。
1 void Dijkstra(int s) //计算从s到其他所有点的最短路 2 { 3 for(int i = 1; i <= n; i++) dis[i] = mp[s][i]; //维护一个记录最短路的数组 4 v[s] = 1; //从s到s的最短路已获得,标记为1 5 int minn, k; 6 for(int i = 1; i <= n; i++) //进行n次,可能小于n提前结束(好像是n-1次?) 7 { 8 minn = M; 9 for(int j = 1; j <= n; j++) //寻找当前已经确定的最短路 10 { 11 if(!v[j] && minn > dis[j]) 12 { 13 minn = dis[j]; 14 k = j; 15 } 16 } 17 if(minn == M) break; //提前结束(即到所有点的距离都以找到,v[]全部为1),则提前退出 18 19 v[k] = 1; //当前最短路已找到,标记为1 20 for(int j = 1; j <= n; j++) //在当前最短路的基础上,寻找到其他未找到点的最短路 21 if(!v[j] && dis[j] > dis[k]+mp[k][j]) dis[j] = dis[k]+mp[k][j]; 22 } 23 }
这道题用Dijkstra跑了15ms。
接下来是BellManFord算法——
这个算法就是进行连续松弛,时间复杂度为O(n*m)不能打折,所以效率比Dijkstra低。但是,不超过10^4的数据一般还是可以在1s内完成的
这个算法的优点是可以计算存在负权的图的最短路,同时它也是求单源最短路的算法。它还可以判断图中是否存在负环(存在负环时是没有最短路的)。
1 bool BellManFord(int s) 2 { 3 for(int i = 1; i <= n; i++) dis[i] = mp[s][i]; 4 for(int i = 1; i < n; i++) 5 { 6 for(int j = 1; j <= n; j++) 7 { 8 for(int k = 1; k <= n; k++) 9 { 10 if(dis[j] > dis[k]+mp[j][k]) dis[j] = dis[k]+mp[j][k]; 11 } 12 } 13 } 14 for(int j = 1; j <= n; j++) //算法的精髓,在求出一般意义的最短路之后,在进行一次判断操作,以确定是否存在负环 15 { 16 for(int k = 1; k <= n; k++) if(dis[j] > dis[k]+mp[j][k]) return 0; //存在负环,返回0 17 } 18 return 1; 19 }
这个算法跑了31ms。
最后是Spfa算法——
这个算法是BellManFord算法的延伸,或者说优化。它的时间复杂度不稳定,不同的图可能算出来的不一样。我目前能写的是用队列的,类似于bfs,据说还有用dfs写的……
总之我对这个算法也不是完全掌握,只是能用而已。
如果Dijkstra也超时的话,可以用这个算法碰碰运气,说不定就过了。
对了,这个算法由于是从BellManFord优化而来的,所以也可以判负环,不过我这个没有写。加一个判断节点入队次数的操作就行了,如果某节点入队大于n次,就是有负环。
这个算法还是很好理解的。
1 void Spfa(int s) 2 { 3 for(int i = 1; i <= n; i++) dis[i] = M; 4 dis[s] = 0; 5 queue<int>que; 6 que.push(s); 7 v[s] = 1; 8 9 int p; 10 while(!que.empty()) 11 { 12 p = que.front(); 13 que.pop(); 14 v[p] = 0; 15 for(int i = 1; i <= n; i++) 16 { 17 if(dis[i] > dis[p]+mp[p][i]) 18 { 19 dis[i] = dis[p]+mp[p][i]; 20 if(v[i] == 0) 21 { 22 que.push(i); 23 v[i] = 1; 24 } 25 } 26 } 27 } 28 29 }
同样跑了15ms。
当然,以上的运行时间并不具有代表性,因为不同的图用不同的算法运行时间总存在差异,尤其是spfa,不过,某种程度上还是可以作为参考的。
整体代码:
1 #include <cstdio> 2 #include <cmath> 3 #include <cstring> 4 #include <algorithm> 5 #include <queue> 6 using namespace std; 7 8 const int M = 100000010; 9 const int N = 110; 10 11 int n, m; 12 int a, b, c; 13 int mp[N][N]; 14 bool v[N]; 15 int dis[N]; 16 17 void Init() 18 { 19 for(int i = 1; i <= n; i++) 20 { 21 for(int j =1; j < i; j++) 22 mp[i][j] = mp[j][i] = M; 23 mp[i][i] = 0; 24 v[i] = 0; 25 } 26 for(int i = 0; i < m; i++) 27 { 28 scanf("%d%d%d", &a, &b, &c); 29 if(mp[a][b] > c) mp[a][b] = mp[b][a] = c; 30 } 31 } 32 33 void Floyd() 34 { 35 for(int k = 1; k <= n; k++) //选择的中间节点 36 for(int i = 1; i <= n; i++) //选择的源节点,即出发点 37 for(int j = 1; j <= n; j++) //选择的目标节点 38 if(mp[i][j] > mp[i][k] + mp[k][j]) 39 mp[i][j] = mp[i][k]+mp[k][j]; //如果经过中间节点的路径(即i到k到j)小于直接从i到j,则进行更新操作(松弛) 40 } 41 42 void Dijkstra(int s) //计算从s到其他所有点的最短路 43 { 44 for(int i = 1; i <= n; i++) dis[i] = mp[s][i]; //维护一个记录最短路的数组 45 v[s] = 1; //从s到s的最短路已获得,标记为1 46 int minn, k; 47 for(int i = 1; i <= n; i++) //进行n次,可能小于n提前结束(好像是n-1次?) 48 { 49 minn = M; 50 for(int j = 1; j <= n; j++) //寻找当前已经确定的最短路 51 { 52 if(!v[j] && minn > dis[j]) 53 { 54 minn = dis[j]; 55 k = j; 56 } 57 } 58 if(minn == M) break; //提前结束(即到所有点的距离都以找到,v[]全部为1),则提前退出 59 60 v[k] = 1; //当前最短路已找到,标记为1 61 for(int j = 1; j <= n; j++) //在当前最短路的基础上,寻找到其他未找到点的最短路 62 if(!v[j] && dis[j] > dis[k]+mp[k][j]) dis[j] = dis[k]+mp[k][j]; 63 } 64 } 65 66 bool BellManFord(int s) 67 { 68 for(int i = 1; i <= n; i++) dis[i] = mp[s][i]; 69 for(int i = 1; i < n; i++) 70 { 71 for(int j = 1; j <= n; j++) 72 { 73 for(int k = 1; k <= n; k++) 74 { 75 if(dis[j] > dis[k]+mp[j][k]) dis[j] = dis[k]+mp[j][k]; 76 } 77 } 78 } 79 for(int j = 1; j <= n; j++) //算法的精髓,在求出一般意义的最短路之后,在进行一次判断操作,以确定是否存在负环 80 { 81 for(int k = 1; k <= n; k++) if(dis[j] > dis[k]+mp[j][k]) return 0; //存在负环,返回0 82 } 83 return 1; 84 } 85 86 void Spfa(int s) 87 { 88 for(int i = 1; i <= n; i++) dis[i] = M; 89 dis[s] = 0; 90 queue<int>que; 91 que.push(s); 92 v[s] = 1; 93 94 int p; 95 while(!que.empty()) 96 { 97 p = que.front(); 98 que.pop(); 99 v[p] = 0; 100 for(int i = 1; i <= n; i++) 101 { 102 if(dis[i] > dis[p]+mp[p][i]) 103 { 104 dis[i] = dis[p]+mp[p][i]; 105 if(v[i] == 0) 106 { 107 que.push(i); 108 v[i] = 1; 109 } 110 } 111 } 112 } 113 114 } 115 116 int main() 117 { 118 while(~scanf("%d%d", &n, &m) && (n+m)) 119 { 120 Init(); 121 //Floyd(); 122 //printf("%d ", mp[1][n]); 123 //Dijkstra(1); 124 //if(BellManFord(1)) 125 Spfa(1); 126 printf("%d ", dis[n]); 127 } 128 return 0; 129 }
总之这只是一个入门等级的最短路,接下来还有一大波优化等着我们呢,几乎每个最短路算法都有优化,用优先队列,用堆,用双向队列……哦,天哪!