标签:style blog http color 使用 os io 数据
本人QQ :2319411771 邮箱 : cyb19950118@163.com
若您发现本文有什么错误,请联系我,我会及时改正的,谢谢您的合作!
本文为原创文章,转载请注明出处
本文链接 :http://www.cnblogs.com/Yan-C/p/3916281.html 。
很早就想写一下最短路的总结了,但是一直懒,就没有写,这几天又在看最短路,岁没什么长进,但还是加深了点理解。
于是就想写一个大点的总结,要写一个全的。
在本文中因为邻接表在比赛中不如前向星好写,而且前向星效率并不低所以,本文的代码 存图只有两种:前向星 or 邻接矩阵
本文包含如下内容:
1、Bellman-Ford算法
2、Dijkstra算法(代码 以邻接矩阵为例) && Dijkstra + 优先队列的优化(也就是堆优化)
3、floyd-Warshall算法(代码 以邻接矩阵为例)
4、SPFA(代码 以前向星为例)
5、BFS 求解最短路
6、路径还原
序章 :
在开始最短路的算法之前,需要先说明一下 松弛 (relax)
松弛是什么? 这个问题在我刚刚开始接触最短路的时候也是一脸茫然啊。但是在读了《算法导论》后我知道了。有资源的同学可以看一下。
不想看厚厚的书的同学看这儿 : 松弛其实很简单,就是 用现在的最小路径去更新其他的路径。用C/C++写其实就是这个样子。
1 if(dis[i]>dis[k]+G[k][i]){ 2 dis[i] = dis[k]+G[k][i]; 3 } 4 //其中dis[i] 是其他的路径 5 //dis[k] 是现在的最小路径 6 //G[k][i] 是现在的最小路径的点到其他路径点的权值。
在《算法导论》中松弛这一步 若条件成立 不仅更新了路径距离 && 更新了前驱。此处未写出。
插播一下 :
在讲完松弛操作之后,最短路算法开始之前,说一下什么是 最短路径的估计值
我们的源点用s表示。
在这里我们用数组 dis[N] 来存储最短路径,dis[N]数组为源点到其他点的最小距离。
那么最最开始的最短路径的估计值 也就是对 dis[N] 的初始化喽。
一般我们的初始化都是初始化为 dis[N] = +∞ , But 在一些时候是初始化为dis[N] = 0的(“一些时候”后面再讲)。
But 源点是要初始化为0的, dis[s] = 0,因为s—>s的距离为0;
1 #define MAX 9999999 2 3 int dis[203]; 4 5 fill(dis,dis+n,MAX);//不知此函数的可以百度 6 dis[s] = 0;
我们也可以这样原始的初始化。
1 #define MAX 9999999 2 3 int dis[203]; 4 int i; 5 6 for(i=0;i<n;i++) 7 dis[i] = MAX; 8 dis[s] = 0;
1、Bellman-Ford算法 :
bellman-ford 算法解决的是一般情况下的单源最短路径问题,其边可以为负值。bellman-ford算法可以判断图是否存在负环,若存在负环会返回一个布尔值。当然在没有负环存在的 情况下会返回所求的最短路径的值。
bellman-ford() :算法如下
1 图的初始化等操作
2 for i = 1 to |G.V| - 1 // |G.V| 为图 G的点的总数
3 for each edge(u,v)∈G.E //G.E 为图 G 的边
4 relax(u,v,w) 也就是if v.d>u.d+w(u,v) , v.d = u.d+w(u,v);
5 for each edge(u,v)∈G.E
6 if v.d>u.d+w(u,v) //v.d为出发源点到结点v的最短路径的估计值 u.d亦如此 w(u,v) 为u结点到v结点的权重值(通俗点就是u—>v的花费)。
7 return false;
8 return true
此算法分为3步:
1) 第1行对图进行初始化,初始化dis[N] = +∞,dis[s] = 0;
2) 第2~4行为求最短路的过程,是对图的每一条边进行|V|-1次松弛操作,求取最短路径。
3) 第5~8行为对每条边进行|V|-1次松弛后,检查是否存在负环并返回相应的布尔值,因为进行|V|-1次松弛后若没有负环则v.d的值确定不变,若有负环则会继续进行松弛操作,因为一个数+负数是一定比它本身要小的。
此算法的 时间复杂度为O(VE)。
eg :
我们做一个简单的题练习一下:
多组输入。第一行给你两个数n(代表点),m(代表边)
第2—m+1行 ,每行三个数u,v, w。0<=u,v<n, w>=0;
第m+2行两个数 s, t 。 s为源点,t为要到达的目的点。
求s到t 的最短路,若存在最短路输出最短路的值,否则输出-1。
这也就是hdu 的题目 传送门
1 #include <cstdio> 2 #include <cstring> 3 #include <algorithm> 4 #include <iostream> 5 #include <queue> 6 #define MAX 9999999 7 8 using namespace std; 9 10 struct node 11 { 12 int u, v, w; 13 }; 14 node edge[2003]; 15 int n, m, s, t; 16 17 void bellman_ford() 18 { 19 int i, j; 20 bool flag;//用于优化的 21 int dis[203];//保存最短路径 22 //初始化 23 fill(dis,dis+n,MAX);//其他点为+∞ 24 dis[s] = 0;//源点初始化为0 25 m = m<<1;//此处和m = 2*m是一样的,因为建立的无向图 26 for(i=1;i<n;i++)//进行|V|-1次 27 { 28 flag = false;//刚刚开始标记为假 29 for(j=0;j<m;j++)//对每个边 30 { 31 //if (v.d>u.d+w(u,v)) 32 if(dis[edge[j].u]>dis[edge[j].v]+edge[j].w){//进行松弛操作 33 dis[edge[j].u] = dis[edge[j].v]+edge[j].w;//松弛成功 34 flag = true;//若松弛成功则标记为真 35 } 36 } 37 if(!flag)//若所有的边i的循环中没有松弛成功的 38 break;//退出循环 39 //此优化可以大大提高效率。 40 } 41 printf("%d\n",dis[t]==MAX?-1:dis[t]);//输出结果 42 } 43 44 int main() 45 { 46 int i; 47 48 while(scanf("%d %d",&n,&m)==2){//输入点的总数n,边的总数m 49 for(i=0;i<m;i++) 50 { 51 scanf("%d %d %d",&edge[i].u,&edge[i].v,&edge[i].w);//每条边的u,v,w的输入 52 edge[i+m].u = edge[i].v;//因为为无向图所以u—>v和v—>u 是一样的 53 edge[i+m].v = edge[i].u;//So... 54 edge[i+m].w = edge[i].w;//So... 55 } 56 scanf("%d %d",&s,&t);//起点和终点 57 bellman_ford();//调用算法部分 58 } 59 return 0; 60 }
说明 : 因为此图w>=0,所以是一定没有负环的,因此没有 3)第5~8行的操作
对于bellman-ford算法 推荐使用结构体数组存储,因为比较方便和简洁,当然也可以用其他的数据结构。
用到的数据结构
1 struct node 2 { 3 int u, v, w;//u 为起点,v为终点,w为u—>v的权值 4 }; 5 node edge[2003];
主函数对边的读取和存储
1 int main() 2 { 3 int i; 4 5 while(scanf("%d %d",&n,&m)==2){//输入点的总数n,边的总数m 6 for(i=0;i<m;i++) 7 { 8 scanf("%d %d %d",&edge[i].u,&edge[i].v,&edge[i].w);//每条边的u,v,w的输入 9 edge[i+m].u = edge[i].v;//因为为无向图所以u—>v和v—>u 是一样的 10 edge[i+m].v = edge[i].u;//So... 11 edge[i+m].w = edge[i].w;//So... 12 } 13 scanf("%d %d",&s,&t);//起点和终点 14 bellman_ford();//调用算法部分 15 } 16 return 0; 17 }
bellman-ford算法求最短路 C/C++版
1 void bellman_ford() 2 { 3 int i, j; 4 bool flag;//用于优化的 5 int dis[203];//保存最短路径 6 //初始化 7 fill(dis,dis+n,MAX);//其他点为+∞ 8 dis[s] = 0;//源点初始化为0 9 m = m<<1;//此处和m = 2*m是一样的,因为建立的无向图 10 for(i=1;i<n;i++)//进行|V|-1次 11 { 12 flag = false;//刚刚开始标记为假 13 for(j=0;j<m;j++)//对每个边 14 { 15 //if (v.d>u.d+w(u,v)) 16 if(dis[edge[j].u]>dis[edge[j].v]+edge[j].w){//进行松弛操作 17 dis[edge[j].u] = dis[edge[j].v]+edge[j].w;//松弛成功 18 flag = true;//若松弛成功则标记为真 19 } 20 } 21 if(!flag)//若所有的边i的循环中没有松弛成功的 22 break;//退出循环 23 //此优化可以大大提高效率。 24 } 25 printf("%d\n",dis[t]==MAX?-1:dis[t]);//输出结果 26 }
对于优化 的解释:若图中存在负环的情况下外循环需要|V|-1次循环,若不存在负环,平均情况下的循环次数是要小于|V|-1次,当所有边没有松弛操作的时候我们就得到了
最后的答案,没有必要继续循环下去,So有了这个简单的优化。
对于bellman-ford算法求最短路 没有负环的情况下已经说明了,下面说一下求负环的强大功能
eg. 题目传送门 点我 题解
1 #include <cstdio> 2 #include <cstring> 3 #include <algorithm> 4 #include <iostream> 5 #include <queue> 6 #define MAX 9999999 7 8 using namespace std; 9 10 struct node 11 { 12 int u, v, w;//u 为起点,v为终点,w为u—>v的权值 13 }; 14 node edge[5203]; 15 int n, m;//n 点数 m 边数 16 17 bool bellman_ford() 18 { 19 int i, j; 20 bool flag; 21 int dis[503];//保存最短路径 22 23 fill(dis,dis+n,MAX);//初始化 24 dis[1] = 0;//因为判断是否有负环,对整个图而言,So s = 1; 25 //一下部分为 2) 第2~4行的操作 26 for(i=1;i<n;i++)//共需进行|V|-1次 27 { 28 flag = false;//优化 初始化为假 29 for(j=0;j<m;j++)//对每一条边 30 { 31 // if u.d>v.d+w(u,v) , u.d = v.d+w(u,v); 32 if(dis[edge[j].u]>dis[edge[j].v]+edge[j].w){//进行松弛 33 dis[edge[j].u] = dis[edge[j].v]+edge[j].w;//松弛操作成功 34 flag = true;//松弛成功变为真 35 } 36 } 37 if(!flag)//若每条边没有松弛 38 break;//跳出循环 39 } 40 // 一下部分为 3) 第5~8行的操作 41 for(i=0;i<m;i++) 42 if(dis[edge[i].u]>dis[edge[i].v]+edge[i].w)//进行|V|-1次操作后 有边还能进行松弛 说明 43 return true;//存在负环 44 return false;//不存在负环 45 } 46 47 int main() 48 { 49 int t, k, i; 50 51 scanf("%d",&t);//输入测试数据的组数 52 while(t-- && scanf("%d %d %d",&n,&m,&k)){//输入点数,正边数,负边数 53 for(i=0;i<m;i++) 54 { 55 scanf("%d %d %d",&edge[i].u,&edge[i].v,&edge[i].w);//输入u,v,w; 56 edge[i+m].u = edge[i].v;//双向 57 edge[i+m].v = edge[i].u;//双向 58 edge[i+m].w = edge[i].w;//双向 59 } 60 m <<= 1;//正边为双向 所以m = m*2; 61 for(i=m;i<m+k;i++) 62 { 63 scanf("%d %d %d",&edge[i].u,&edge[i].v,&edge[i].w);//存负边数(单向) 64 edge[i].w = -edge[i].w;//负边就要是负的 65 } 66 m += k;//单向,So不需要*2 67 printf("%s\n",bellman_ford()?"YES":"NO");//输出结果 68 } 69 return 0; 70 }
题目大意: 第一行 输入一个数 是表示几组测试数据
第二行 三个数 N(点的个数),M(正边的个数),W(负边的个数) 注意 :正边为双向的,负边为单向的。
然后 M行u,v,w;
再然后W行u,v,w;
求这个图是不是存在负环。 有 YES 没NO。
所用数据结构 :
1 struct node 2 { 3 int u, v, w;//u 为起点,v为终点,w为u—>v的权值 4 }; 5 node edge[5203];
主函数对数据的获取。
1 int main() 2 { 3 int t, k, i; 4 5 scanf("%d",&t);//输入测试数据的组数 6 while(t-- && scanf("%d %d %d",&n,&m,&k)){//输入点数,正边数,负边数 7 for(i=0;i<m;i++) 8 { 9 scanf("%d %d %d",&edge[i].u,&edge[i].v,&edge[i].w);//输入u,v,w; 10 edge[i+m].u = edge[i].v;//双向 11 edge[i+m].v = edge[i].u;//双向 12 edge[i+m].w = edge[i].w;//双向 13 } 14 m <<= 1;//正边为双向 所以m = m*2; 15 for(i=m;i<m+k;i++) 16 { 17 scanf("%d %d %d",&edge[i].u,&edge[i].v,&edge[i].w);//存负边数(单向) 18 edge[i].w = -edge[i].w;//负边就要是负的 19 } 20 m += k;//单向,So不需要*2 21 printf("%s\n",bellman_ford()?"YES":"NO");//输出结果 22 } 23 return 0; 24 }
bellman-ford 算法求解
1 bool bellman_ford() 2 { 3 int i, j; 4 bool flag; 5 int dis[503];//保存最短路径 6 7 fill(dis,dis+n,MAX);//初始化 8 dis[1] = 0;//因为判断是否有负环,对整个图而言,So s = 1; 9 //一下部分为 2) 第2~4行的操作 10 for(i=1;i<n;i++)//共需进行|V|-1次 11 { 12 flag = false;//优化 初始化为假 13 for(j=0;j<m;j++)//对每一条边 14 { 15 // if u.d>v.d+w(u,v) , u.d = v.d+w(u,v); 16 if(dis[edge[j].u]>dis[edge[j].v]+edge[j].w){//进行松弛 17 dis[edge[j].u] = dis[edge[j].v]+edge[j].w;//松弛操作成功 18 flag = true;//松弛成功变为真 19 } 20 } 21 if(!flag)//若每条边没有松弛 22 break;//跳出循环 23 } 24 // 一下部分为 3) 第5~8行的操作 25 for(i=0;i<m;i++) 26 if(dis[edge[i].u]>dis[edge[i].v]+edge[i].w)//进行|V|-1次操作后 有边还能进行松弛 说明 27 return true;//存在负环 28 return false;//不存在负环 29 }
上面只是第一种对负环存在的判断,继续下一种:
我们前面已经说过 若没有负环外循环最多进行|V|-1次即可,就可得到最短路径,那么若存在负环,则第|V|次操作就说明存在负环。
1 bool bellman_ford() 2 { 3 int i, j; 4 bool flag; 5 int dis[503];//保存最短路径 6 7 fill(dis,dis+n,MAX);//初始化 8 dis[1] = 0;//因为判断是否有负环,对整个图而言,So s = 1; 9 //一下部分为 2) 第2~4行的操作 10 for(i=0;i<n;i++)//共需进行|V|-1次 11 { 12 flag = false;//优化 初始化为假 13 for(j=0;j<m;j++)//对每一条边 14 { 15 // if u.d>v.d+w(u,v) , u.d = v.d+w(u,v); 16 if(dis[edge[j].u]>dis[edge[j].v]+edge[j].w){//进行松弛 17 dis[edge[j].u] = dis[edge[j].v]+edge[j].w;//松弛操作成功 18 flag = true;//松弛成功变为真 19 } 20 } 21 if(!flag)//若每条边没有松弛 22 break;//跳出循环 23 //下面这一部分代替了 3) 第5~8行的操作 24 //因为对于V个点 你最多需要进行|V|-1次外循环,如果有负环它会一直进行下去,但是只要进行到第V次的时候就说明存在负环了 25 if(i == n-1)//若有 26 return true;//返回有负环 27 } 28 return false;//不存在负环 29 }
bellman-ford 算法也说的差不多了,对于此算法的SPFA优化 ,我们在本文的后面部分单独讲解。
不知大家还记不记的上面的那个许多个But 中的那个But dis[N] = 0呢?
给大家留下一个问题吧。
这个问题是《算法导论》上的一个思考题,问题是这样的 :
你如果知道一个带权重的有向图中 存在一个负环,那么请你设计一个有效&&正确的算法列出所有属于该环路上的结点。
如果你有什么想法 请跟我一起分享下吧
本人QQ :2319411771 邮箱 : cyb19950118@163.com
2、dijkstra算法 : (贪心策略)
Dijkstra算法解决的是带权重的有向图上单源最短路径问题,该算法要求所有边的权重都为正值。
在此有的同学可能就要问了,为什么不能处理负值呢?
Why????
Dijkstra算法不是绝对的不能处理权重为负值,而是因为这个负值的大小和所在位置需要特别要求才可应用&&求得正确结果。
但我们的平时所遇到的是一般情况下的,是需要算法有通用性的,所以就要求所有边的权重都为正值。
在本文我此算法的后面部分我会给出两个例子,分别为 不可以有负边 和 可以有负边的例子。为什么在此不先给出呢?
Why?????
如果还不知道Dijkstra算法又怎么会 看懂这两个例子呢? So 看完这个算法 再看例子吧。
Dijkstra算法在运行过程中维持的关键信息是一组结点集合S。从源结点s 到该集合中每个结点之间的最短路径都已经被找到。算法重复从结点集V-S中选择最短路径估计最小的结点u,讲u加入到 集合S,然后对所有从u发出的边进行松弛。
Dijkstra 算法如下://这个描述使用的最小优先队列Q来保存结点集合,每个结点的关键值为其d值。
1 对图的建立和处理,dis[N]数组的初始化等等操作
2 S = ∅
3 Q = G.V
4 while Q ≠ ∅
5 u = EXTRACT-MIN(Q)
6 S = S ∪ {u}
7 for each vertex v∈ G.Adj[u]
8 relax(u,v,w)
此算法在此分为二步 : 第二大步中又分为3小步
1) 第1~3行 对dis[N]数组等的初始化,集合S 为∅,Q集合为G.V操作
2) 第4~8行 ① 第4行 进行G.V次操作
② 第5~行 从Q中找到一个点,这个点是Q中所有的点 s—>某点 最小的最短路径的点,并将此点加入S集合
③ 第7~8行 进行松弛操作,用此点来更新其他路径的距离。
对于邻接矩阵存储的图 来说此算法的时间复杂度为 O(|V|²),用其他的数据结构可以优化为O(|E|log|V|)的时间复杂度。
对于本文所说的其他数据结构 使用的为前向星,对于前向星是不能出现负边的。
我们先看邻接矩阵存储的图的情况。
还是hdu的那道题 题目传送门 题解 :
1 #include <cstdio> 2 #include <cstring> 3 #include <algorithm> 4 #include <iostream> 5 #include <queue> 6 #define MAX 9999999 7 8 using namespace std; 9 10 int G[203][203];//二维数组 图的存储 11 int n, s, t;//n 点的个数 , s 起点 ,t 终点 12 13 void dijkstra() 14 { 15 bool vis[203];//相当于集合Q的功能, 标记该点是否访问过 16 int dis[203];//保存最短路径 17 int i, j, k; 18 19 for(i=0;i<n;i++)//初始化 20 dis[i] = G[s][i];//s—>各个点的距离 21 memset(vis,false,sizeof(vis));//初始化为假 表示未访问过 22 dis[s] = 0;//s->s 距离为0 23 vis[s] = true;//s点访问过了,标记为真 24 for(i=1;i<n;i++)//G.V-1次操作+上面对s的访问 = G.V次操作 25 { 26 k = -1; 27 for(j=0;j<n;j++)//从尚未访问过的点中选一个距离最小的点 28 if(!vis[j] && (k==-1||dis[k]>dis[j]))//未访问过 && 是距离最小的 29 k = j; 30 if(k == -1)//若图是不连通的则提前结束 31 break;//跳出循环 32 vis[k] = true;//将k点标记为访问过了 33 for(j=0;j<n;j++)//松弛操作 34 if(!vis[j] && dis[j]>dis[k]+G[k][j])//该点为访问过 && 可以进行松弛 35 dis[j] = dis[k]+G[k][j];//j点的距离 大于当前点的距离+w(k,j) 则松弛成功,进行更新 36 } 37 printf("%d\n",dis[t]==MAX?-1:dis[t]);//输出结果 38 } 39 40 int main() 41 { 42 int m, i, j, u, v, w; 43 44 while(scanf("%d %d",&n,&m)==2){//获取点的个数 边的个数 45 for(i=0;i<n;i++) 46 for(j=0;j<n;j++) 47 G[i][j] = i==j?0:MAX;//初始化,本身到本身的距离为0,其他的为无穷大 48 while(m--){ 49 scanf("%d %d %d",&u,&v,&w);//获取u,v,w(u,v); 50 if(G[u][v]>w)//因为初始化的操作 && 若有重边要去最小的权重值 51 G[u][v] = G[v][u] = w;//无向图 双向 52 } 53 scanf("%d %d",&s,&t);//获取起止点 54 dijkstra(); 55 } 56 return 0; 57 }
应用的数据结构
1 int G[203][203];//二维数组 图的存储
主函数对数据的获取
1 int main() 2 { 3 int m, i, j, u, v, w; 4 5 while(scanf("%d %d",&n,&m)==2){//获取点的个数 边的个数 6 for(i=0;i<n;i++) 7 for(j=0;j<n;j++) 8 G[i][j] = i==j?0:MAX;//初始化,本身到本身的距离为0,其他的为无穷大 9 while(m--){ 10 scanf("%d %d %d",&u,&v,&w);//获取u,v,w(u,v); 11 if(G[u][v]>w)//因为初始化的操作 && 若有重边要去最小的权重值 12 G[u][v] = G[v][u] = w;//无向图 双向 13 } 14 scanf("%d %d",&s,&t);//获取起止点 15 dijkstra(); 16 } 17 return 0; 18 }
Dijkstra算法
1 void dijkstra() 2 { 3 bool vis[203];//相当于集合Q的功能, 标记该点是否访问过 4 int dis[203];//保存最短路径 5 int i, j, k; 6 7 for(i=0;i<n;i++)//初始化 8 dis[i] = G[s][i];//s—>各个点的距离 9 memset(vis,false,sizeof(vis));//初始化为假 表示未访问过 10 dis[s] = 0;//s->s 距离为0 11 vis[s] = true;//s点访问过了,标记为真 12 for(i=1;i<n;i++)//G.V-1次操作+上面对s的访问 = G.V次操作 13 { 14 k = -1; 15 for(j=0;j<n;j++)//从尚未访问过的点中选一个距离最小的点 16 if(!vis[j] && (k==-1||dis[k]>dis[j]))//未访问过 && 是距离最小的 17 k = j; 18 if(k == -1)//若图是不连通的则提前结束 19 break;//跳出循环 20 vis[k] = true;//将k点标记为访问过了 21 for(j=0;j<n;j++)//松弛操作 22 if(!vis[j] && dis[j]>dis[k]+G[k][j])//该点为访问过 && 可以进行松弛 23 dis[j] = dis[k]+G[k][j];//j点的距离 大于当前点的距离+w(k,j) 则松弛成功,进行更新 24 } 25 printf("%d\n",dis[t]==MAX?-1:dis[t]);//输出结果 26 }
另一种 用其他的数据结构可以优化为O(|E|log|V|)的时间复杂度。
使用STL中的最小优先队列 priority_queue,进行优化。
题目继续使用此题。
1 #include <cstdio> 2 #include <cstring> 3 #include <algorithm> 4 #include <iostream> 5 #include <queue> 6 #define MAX 9999999 7 8 using namespace std; 9 //pair 的first 保存的为最短距离, second保存的为顶点编号 10 typedef pair<int, int >P;//对组 不知道请自行百度 11 12 struct node 13 { 14 int v, w;//v 为到达的点, w为权重 15 int next;//记录下一个结构体的位置 ,就向链表的next功能是一样的 16 }; 17 node edge[2003];//存所有的边,因为是无向图,所以*2 18 int cnt;//结构体的下标 19 int n, s, t;//n 点数,s 起点,t止点 20 int head[203];//和链表的头指针数组是一样的。只不过此处head[u]记录的为最后加入 edge 的且与u相连的边在 edge 中的位置,即下标 21 22 void add(int u, int v, int w)//加边操作 23 { 24 edge[cnt].v = v; 25 edge[cnt].w = w; 26 edge[cnt].next = head[u];//获得下一个结构体的位置 27 head[u] = cnt++;//记录头指针的下标 28 } 29 30 void dijkstra() 31 { 32 int dis[203];//最短路径数组 33 int i, v;//v保存从队列中取出的数的第二个数 也就是顶点的编号 34 priority_queue<P,vector<P>,greater<P> >que;//优先队列 从小到大 35 node e;//保存边的信息,为了书写方便 36 P p;//保存从队列取出的数值 37 38 fill(dis,dis+n,MAX);//初始化,都为无穷大 39 dis[s] = 0;//s—>s 距离为0 40 que.push(P(0,s));//放入距离 为0 点为s 41 while(!que.empty()){ 42 p = que.top();//取出队列中最短距离最小的对组 43 que.pop();//删除 44 v = p.second;//获得最短距离最小的顶点编号 45 if(dis[v] < p.first)//若取出的不是最短距离 46 continue;//则进行下一次循环 47 for(i=head[v];i!=-1;i=edge[i].next)//对与此点相连的所有的点进行遍历 48 { 49 e = edge[i];//为了书写的方便。 50 if(dis[e.v]>dis[v]+e.w){//进行松弛 51 dis[e.v]=dis[v]+e.w;//松弛成功 52 que.push(P(dis[e.v],e.v));//讲找到的松弛成功的距离 和顶点放入队列 53 } 54 } 55 } 56 printf("%d\n",dis[t]==MAX?-1:dis[t]);//输出结果 57 } 58 59 int main() 60 { 61 int m, u, v, w; 62 63 while(scanf("%d %d",&n,&m)==2){//获取点数 边数 64 cnt = 0;//结构体下标从0开始 65 memset(head,-1,sizeof(head));//初始化head[N]数组 66 while(m--){ 67 scanf("%d %d %d",&u,&v,&w);//获取u,v,w(u,v) 68 add(u,v,w);//加边 69 add(v,u,w);//加边 70 } 71 scanf("%d %d",&s,&t);//获取起止点 72 dijkstra(); 73 } 74 return 0; 75 }
用到的数据结构 :前向星 对组 优先队列
1 //pair 的first 保存的为最短距离, second保存的为顶点编号 2 typedef pair<int, int >P;//对组 不知道请自行百度 3 4 struct node//前向星存边 5 { 6 int v, w;//v 为到达的点, w为权重 7 int next;//记录下一个结构体的位置 ,就向链表的next功能是一样的 8 }; 9 node edge[2003];//存所有的边,因为是无向图,所以*2 10 int head[203];//和链表的头指针数组是一样的。只不过此处head[u]记录的为最后加入 edge 的且与u相连的边在 edge 中的位置,即下标 11 12 priority_queue<P,vector<P>,greater<P> >que;//优先队列 从小到大
在此我们说一下前向星的加边函数
1 void add(int u, int v, int w)//加边操作 2 { 3 edge[cnt].v = v; 4 edge[cnt].w = w; 5 edge[cnt].next = head[u];//获得下一个结构体的位置 6 head[u] = cnt++;//记录头指针的下标 7 }
主函数对数据的获取
1 int main() 2 { 3 int m, u, v, w; 4 5 while(scanf("%d %d",&n,&m)==2){//获取点数 边数 6 cnt = 0;//结构体下标从0开始 7 memset(head,-1,sizeof(head));//初始化head[N]数组 8 while(m--){ 9 scanf("%d %d %d",&u,&v,&w);//获取u,v,w(u,v) 10 add(u,v,w);//加边 11 add(v,u,w);//加边 12 } 13 scanf("%d %d",&s,&t);//获取起止点 14 dijkstra(); 15 } 16 return 0; 17 }
Dijkstra算法求值
1 void dijkstra() 2 { 3 int dis[203];//最短路径数组 4 int i, v;//v保存从队列中取出的数的第二个数 也就是顶点的编号 5 priority_queue<P,vector<P>,greater<P> >que;//优先队列 从小到大 6 node e;//保存边的信息,为了书写方便 7 P p;//保存从队列取出的数值 8 9 fill(dis,dis+n,MAX);//初始化,都为无穷大 10 dis[s] = 0;//s—>s 距离为0 11 que.push(P(0,s));//放入距离 为0 点为s 12 while(!que.empty()){ 13 p = que.top();//取出队列中最短距离最小的对组 14 que.pop();//删除 15 v = p.second;//获得最短距离最小的顶点编号 16 if(dis[v] < p.first)//若取出的不是最短距离 17 continue;//则进行下一次循环 18 for(i=head[v];i!=-1;i=edge[i].next)//对与此点相连的所有的点进行遍历 19 { 20 e = edge[i];//为了书写的方便。 21 if(dis[e.v]>dis[v]+e.w){//进行松弛 22 dis[e.v]=dis[v]+e.w;//松弛成功 23 que.push(P(dis[e.v],e.v));//讲找到的松弛成功的距离 和顶点放入队列 24 } 25 } 26 } 27 printf("%d\n",dis[t]==MAX?-1:dis[t]);//输出结果 28 }
自此Dijkstra算法就算接近尾声了,现在还大家一个债,那就是前面的Why
在此给出的是百度知道上的一位网友给的解释 :
dijkstra由于是贪心的,每次都找一个距源点最近的点(dmin),然后将该距离定为这个点到源点的最短路径(d[i]<--dmin);但如果存在负权边,那就有可能先通过并不是距源点最近的一个次优点(dmin‘),再通过这个负权边L(L<0),使得路径之和更小(dmin‘+L<dmin),则dmin‘+L成为最短路径,并不是dmin,这样dijkstra就被囧掉了
比如n=3,邻接矩阵:
0,3,4
3,0,-2
4,-2, 0
用dijkstra求得d[1,2]=3,事实上d[1,2]=2,就是通过了1-3-2使得路径减小。
这就是为什么Dijkstra不能处理负边的情况。
再给出可以使用Dijkstra && 带负边的情况
n = 3,邻接矩阵
0, 3, 4
3, 0, -1
4, -1, 0
dis[1,2] = 3, dis[1,3] = 2 是正确的。(为邻接矩阵的存图方式下的)
此算法讲解结束。
3、floyd-Warshall算法 : (动态规划)
floyd算法是一个很强大的算法,它可以计算任意两点之间的最短路径,其边可以为负值。
对于floyd算法是我刚刚开始接触最短路算法中最喜欢的了,因为它的代码简短,便于理解,而且功能也很强大,虽然有点短腿但我还是很喜欢这个代码。
floyd算法是三重for 的嵌套。对于这个算法给出《挑战程序设计》中的证明 :
证明:
对于0~k,我们分i到j的最短路正好经过顶点k一次和完全不经过顶点k两种情况来讨论。不仅过顶点k的情况下,d[k][i][j] = d[k-1][i][j]。通过顶点k的情况,d[k][i][j]
= d[k-1][i][k]+d[k-1][k][j]。合起来就得到了d[k][i][j] = min(d[k-1][i][j],d[k-1][i][k]+d[k-1][k][j])。这个DP也可以用同一个数组不断进行如下的操作:
d[i][j] = min(d[i][j],d[i][k]+d[k][j])的更新来实现。
floyd算法的时间复杂度为O(|V|³)。 450*450*450<10的8次方
下面给出floyd算法的程序。
1 void floyd() 2 { 3 int i, j, k; 4 5 for(k=0;k<n;k++) 6 for(i=0;i<n;i++) 7 for(j=0;j<n;j++) 8 G[i][j] = min(G[i][j],G[i][k]+G[k][j]); 9 printf("%d\n",G[s][t]==MAX?-1:G[s][t]); 10 }
在此给出图的初始化和数据的读取。
1 int main() 2 { 3 int i, j, m, u, v, w; 4 5 while(scanf("%d %d",&n,&m)==2){ 6 for(i=0;i<n;i++) 7 for(j=0;j<n;j++) 8 G[i][j] = i==j?0:MAX; 9 while(m--){ 10 scanf("%d %d %d",&u,&v,&w); 11 if(G[u][v]>w) 12 G[u][v] = G[v][u] = w; 13 } 14 scanf("%d %d",&s,&t); 15 floyd(); 16 } 17 return 0; 18 }
对floyd算法呢,因为他的简洁,在此就不多说。
补充一下:对于floyd判断负环是否存在只需检查是否存在d[i][i]是负数的顶点i 即可。
4、 SPFA算法 (bellman-ford算法的优化)
SPFA算法是西南交通大学段凡丁于1994年发表的。
SPFA算法 :设立一个先进先出的队列用来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作,如果v点的最短 路径估计值有所调整,且v点不在当前的队列中,就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止。
SPFA 是这样判断负环的: 如果某个点进入队列的次数超过N次则存在负环(SPFA无法处理带负环的图)
期望的时间复杂度:O(ke), 其中k为所有顶点进队的平均次数,可以证明k一般小于等于2。
SPFA算法有两个优化算法 SLF 和 LLL: SLF:Small Label First 策略,设要加入的节点是j,队首元素为i,若dist(j)<dist(i),则将j插入队首,否则插入队尾。 LLL:Large Label Last 策略,设队首元素为i,队列中所有dist值的平均值为x,若dist(i)>x则将i插入到队尾,查找下一元素,直到找到某一i使得dist(i)<=x,则将i出队进行松弛操作。 SLF 可使速度提高 15 ~ 20%;SLF + LLL 可提高约 50%。 在实际的应用中SPFA的算法时间效率不是很稳定,为了避免最坏情况的出现,通常使用效率更加稳定的Dijkstra算法。
SPFA() :
1 对图的建立和处理,dis[N]数组的初始化等等操作
2 Q += s //Q 为一个队列 s为源点
3 while Q ≠ ∅//队列不为空
4 u = Q中的点//从Q中取出一个点u
5 把u点标记为为访问过的
6 for each vertex v∈ G.Adj[u]//对所有的边
7 relax(u,v,w)//进行松弛
8 if(v 未被访问过)//若v未被访问过
9 Q += v;//加入队列
以上伪代码为自己写的,希望能看。
此算法分为3部分 :
1) 第1~2行 建图对dis[N]和vis[N]数组等数组进行初始化。 若判断负环需要加一个flag[N]数组,初始化为0,某点 u 若加入Q队列一次,怎flag[u]++,若flag[u]>=n,说明u进入队列的次数大于点的个数,因此此图存在负环,返回一个布尔值。
2) 第3行当队列不为空的时候进行操作
3) 第4~9行 取出Q中的点u ,用u对所有的边进行松弛操作,若松弛成功,判断该点v是否被访问过,若未访问过加入Q队列中。
继续以poj的虫洞为例题
1 #include <cstdio> 2 #include <cstring> 3 #include <algorithm> 4 #include <iostream> 5 #include <queue> 6 #define MAX 9999999 7 8 using namespace std; 9 10 int G[503][503]; 11 int n; 12 13 bool SPFA() 14 { 15 int u; 16 int i; 17 queue<int >que; 18 int dis[503]; 19 bool vis[503]; 20 int flag[503]; 21 22 memset(flag,0,sizeof(flag)); 23 memset(vis,false,sizeof(vis)); 24 fill(dis,dis+n+1,MAX); 25 dis[1] = 0; 26 que.push(1); 27 while(!que.empty()){ 28 u = que.front(); 29 que.pop(); 30 vis[u] = false; 31 for(i=1;i<=n;i++) 32 { 33 if(dis[i]>dis[u]+G[u][i]){ 34 dis[i] = dis[u]+G[u][i]; 35 if(!vis[i]){ 36 vis[i] = true; 37 flag[i]++; 38 if(flag[i]>=n) 39 return true; 40 que.push(i); 41 } 42 } 43 } 44 } 45 return false; 46 } 47 48 int main() 49 { 50 int t, k, i, j, u, v, w, m; 51 52 scanf("%d",&t); 53 while(t--){ 54 scanf("%d %d %d",&n,&m,&k); 55 for(i=1;i<=n;i++) 56 for(j=1;j<=n;j++) 57 G[i][j] = i==j?0:MAX; 58 for(i=0;i<m;i++) 59 { 60 scanf("%d %d %d",&u,&v,&w); 61 if(G[u][v]>w) 62 G[u][v] = G[v][u] = w; 63 } 64 for(i=0;i<k;i++) 65 { 66 scanf("%d %d %d",&u,&v,&w); 67 G[u][v] = -w; 68 } 69 printf("%s\n",SPFA()?"YES":"NO"); 70 } 71 return 0; 72 }
SPFA算法
1 bool SPFA() 2 { 3 int u;//从队列Q中取出的数 4 int i; 5 queue<int >que;//Q队列 6 int dis[503];//保存最短距离 7 bool vis[503];//访问数组 8 int flag[503];//记录点进入队列的次数 9 10 memset(flag,0,sizeof(flag));//初始化为0 11 memset(vis,false,sizeof(vis));//初始化 12 fill(dis,dis+n+1,MAX);//初始化 13 dis[1] = 0;//从 1 开始 14 que.push(1);//将 1 放入队列 15 while(!que.empty()){//Q 不为空 16 u = que.front();//从Q中取出一个数 17 que.pop();//删除此数 18 vis[u] = false;//标记为未访问过 19 for(i=1;i<=n;i++)//对所有的边 20 { 21 if(dis[i]>dis[u]+G[u][i]){//进行松弛 22 dis[i] = dis[u]+G[u][i];//松弛成功 23 if(!vis[i]){//若点i 未被访问过 24 vis[i] = true;//标记为访问过 25 flag[i]++;//入队列次数+1 26 if(flag[i]>=n)//若此点进入队列此数超过n次 说明有负环 27 return true;//有负环 28 que.push(i);//将 此点放入队列 29 } 30 } 31 } 32 } 33 return false;//没有负环 34 }
SPFA算法对于稀疏图才能发挥它的大作用,对于稀疏图我们用到的数据结构为 前向星
下面就是 SPFA+前向星的程序 并应用了SLF 双向队列进行优化
1 #include <cstdio> 2 #include <cstring> 3 #include <algorithm> 4 #include <iostream> 5 #include <queue> 6 #define MAX 9999999 7 8 using namespace std; 9 10 struct node//前向星 11 { 12 int v,w;//v 终点,w 权值 13 int next;//下一个 14 }; 15 node edge[5203];//前向星 16 int head[503];//头指针式的数组 17 int cnt;//下标 18 int n;//点的个数 19 20 void add(int u, int v, int w)//加边 建议 若看不懂前向星 请自己动手画一下 按照给出的数据和程序 21 { 22 edge[cnt].v = v;// 23 edge[cnt].w = w;// 24 edge[cnt].next = head[u];// 25 head[u] = cnt++;// 26 } 27 28 bool SPFA() 29 { 30 int i, u, v;//u 从Q中取出的点 v找到的点 31 int dis[503];//保存最短路径 32 int flag[503];//保存某点加入队列的次数 33 bool vis[503];//标记数组 34 deque<int>que;//双向队列 自己百度 35 36 fill(dis,dis+n+1,MAX);//初始化 37 memset(flag,0,sizeof(flag));//初始化 38 memset(vis,false,sizeof(vis));//初始化 39 dis[1] = 0;// s为1 40 que.push_back(1);//将s = 1 加入队列 41 while(!que.empty()){//当队列不为空 42 u = que.front();//从队列中取出一个数 43 que.pop_front();//删除 44 vis[u] = false;//标记为未访问 45 for(i=head[u];i!=-1;i=edge[i].next)//对所有与该边相连的边进行查找 46 { 47 v = edge[i].v;//保存点 便于操作 48 if(dis[v]>dis[u]+edge[i].w){//进行松弛操作 49 dis[v] = dis[u]+edge[i].w;//松弛成功 50 if(!vis[v]){//若该点未被标记 51 vis[v] = true;//标记为真 52 flag[v]++;//该点入队列次数++ 53 if(flag[v]>=n)//若该点进入队列次数超过n次 说明有负环 54 return true;//返回有负环 55 //一下为SLF优化 56 if(!que.empty() && dis[v]<dis[que.front()])//若为队列不为空 && 队列第一个点的最短距离大于当前点的最短距离 57 que.push_front(v);//将该点放到队首 58 else//不然 59 que.push_back(v);//放入队尾 60 } 61 } 62 } 63 } 64 return false;//没有负环 65 } 66 67 int main() 68 { 69 int u, v, w, m, k, t; 70 71 scanf("%d",&t);//获取测试数据 72 while(t--){ 73 memset(head,-1,sizeof(head));//初始化 74 cnt = 0;//下标为0 初始化 75 scanf("%d %d %d",&n,&m,&k);//获取点的个数 ,正边的个数, 负边的个数 76 while(m--){ 77 scanf("%d %d %d",&u,&v,&w);//正边获取 78 add(u,v,w);//无向图 79 add(v,u,w);//双向建图 80 } 81 while(k--){ 82 scanf("%d %d %d",&u,&v,&w); 83 add(u,v,-w);//单向图 84 } 85 printf("%s\n",SPFA()?"YES":"NO");//输出结果 86 } 87 return 0; 88 }
所用数据结构有 前向星 双向队列
1 struct node//前向星 2 { 3 int v,w;//v 终点,w 权值 4 int next;//下一个 5 }; 6 node edge[5203];//前向星 7 int head[503];//头指针式的数组 8 int cnt;//下标 9 10 deque<int>que;//双向队列 自己百度
主函数 对数据的获取 和图的建立
1 int main() 2 { 3 int u, v, w, m, k, t; 4 5 scanf("%d",&t);//获取测试数据 6 while(t--){ 7 memset(head,-1,sizeof(head));//初始化 8 cnt = 0;//下标为0 初始化 9 scanf("%d %d %d",&n,&m,&k);//获取点的个数 ,正边的个数, 负边的个数 10 while(m--){ 11 scanf("%d %d %d",&u,&v,&w);//正边获取 12 add(u,v,w);//无向图 13 add(v,u,w);//双向建图 14 } 15 while(k--){ 16 scanf("%d %d %d",&u,&v,&w); 17 add(u,v,-w);//单向图 18 } 19 printf("%s\n",SPFA()?"YES":"NO");//输出结果 20 } 21 return 0; 22 }
SPFA+前向星
1 bool SPFA() 2 { 3 int i, u, v;//u 从Q中取出的点 v找到的点 4 int dis[503];//保存最短路径 5 int flag[503];//保存某点加入队列的次数 6 bool vis[503];//标记数组 7 deque<int>que;//双向队列 自己百度 8 9 fill(dis,dis+n+1,MAX);//初始化 10 memset(flag,0,sizeof(flag));//初始化 11 memset(vis,false,sizeof(vis));//初始化 12 dis[1] = 0;// s为1 13 que.push_back(1);//将s = 1 加入队列 14 while(!que.empty()){//当队列不为空 15 u = que.front();//从队列中取出一个数 16 que.pop_front();//删除 17 vis[u] = false;//标记为未访问 18 for(i=head[u];i!=-1;i=edge[i].next)//对所有与该边相连的边进行查找 19 { 20 v = edge[i].v;//保存点 便于操作 21 if(dis[v]>dis[u]+edge[i].w){//进行松弛操作 22 dis[v] = dis[u]+edge[i].w;//松弛成功 23 if(!vis[v]){//若该点未被标记 24 vis[v] = true;//标记为真 25 flag[v]++;//该点入队列次数++ 26 if(flag[v]>=n)//若该点进入队列次数超过n次 说明有负环 27 return true;//返回有负环 28 //一下为SLF优化 29 if(!que.empty() && dis[v]<dis[que.front()])//若为队列不为空 && 队列第一个点的最短距离大于当前点的最短距离 30 que.push_front(v);//将该点放到队首 31 else//不然 32 que.push_back(v);//放入队尾 33 } 34 } 35 } 36 } 37 return false;//没有负环 38 }
好了,四种算法已经讲完。
5、 BFS 求解最短路
在我们学习图的基本算法BFS 和DFS的时候,其实那就是一个求解每一步的权重都为1的最短路,那么权重不为1的情况我,我们是否继续使用呢?
答案是肯定的。
采用邻接表或前向星进行图的存储 , 则BFS的时间复杂度为 开始的初始化O(V)+BFS操作O(E) = O (V+E)
继续以hdu 的 畅通工程续 为例
1 #include <algorithm> 2 #include <iostream> 3 #include <cstdio> 4 #include <cstring> 5 #include <queue> 6 7 using namespace std; 8 9 struct P 10 { 11 int v, w;//v 顶点 w 最短距离 12 bool operator <(const P &a)const{ 13 return a.w < w;//按w 从小到大排序 14 } 15 }; 16 struct node//前向星 17 { 18 int v, w;//v 顶点 w权重 19 int next;//下一个位置 20 }; 21 node edge[2003]; 22 int head[203];//头指针数组 23 int cnt, s, t;// cnt 下标 24 25 void add(int u, int v, int w)//加边操作 26 { 27 edge[cnt].v = v; 28 edge[cnt].w = w; 29 edge[cnt].next = head[u]; 30 head[u] = cnt++; 31 } 32 33 void BFS() 34 { 35 priority_queue<P>que;//优先队列 按w从小到大 36 bool vis[203];//标记数组, 标记是否被访问过 37 P p, q; 38 int i, v; 39 40 memset(vis,false,sizeof(vis));//初始化 41 p.v = s;//顶点为 s 42 p.w = 0;//距离为 0 43 que.push(p);//放入队列 44 while(que.empty() == false){//队列不为空 45 p = que.top();//取出队列的队首 46 que.pop();//删除 47 if(p.v == t){//若找到终点 48 printf("%d\n",p.w);//输出结果 49 return ;//返回 50 } 51 vis[p.v] = true;//此点标记为访问过 52 for(i=head[p.v];i!=-1;i=edge[i].next)//查找与该点相连的点 53 { 54 v = edge[i].v; 55 if(vis[v] == false){//若点未被访问过 56 q.v = v;//存入结构体 57 q.w = p.w+edge[i].w;//距离更新 58 que.push(q);//放入队列 59 } 60 } 61 } 62 printf("-1\n");//若没有到达终点 输出-1 63 } 64 65 int main() 66 { 67 int m, u, v, w, n; 68 69 while(scanf("%d %d",&n,&m)==2){//获取点的个数 边的个数 70 memset(head,-1,sizeof(head));//初始化 71 cnt = 0;//初始化 72 while(m--){ 73 scanf("%d %d %d",&u,&v,&w);//获取u,v,w 74 add(u,v,w);//加边 75 add(v,u,w);//无向图 双向加边 76 } 77 scanf("%d %d",&s,&t);//获取起止点 78 BFS(); 79 } 80 return 0; 81 }
所用数据结构 前向星 优先队列
1 struct P 2 { 3 int v, w;//v 顶点 w 最短距离 4 bool operator <(const P &a)const{ 5 return a.w < w;//按w 从小到大排序 6 } 7 }; 8 priority_queue<P>que;//优先队列 按w从小到大 9 struct node//前向星 10 { 11 int v, w;//v 顶点 w权重 12 int next;//下一个位置 13 }; 14 node edge[2003]; 15 int head[203];//头指针数组
主函数对数据的获取
1 int main() 2 { 3 int m, u, v, w, n; 4 5 while(scanf("%d %d",&n,&m)==2){//获取点的个数 边的个数 6 memset(head,-1,sizeof(head));//初始化 7 cnt = 0;//初始化 8 while(m--){ 9 scanf("%d %d %d",&u,&v,&w);//获取u,v,w 10 add(u,v,w);//加边 11 add(v,u,w);//无向图 双向加边 12 } 13 scanf("%d %d",&s,&t);//获取起止点 14 BFS(); 15 } 16 return 0; 17 }
加边操作
1 void add(int u, int v, int w)//加边操作 2 { 3 edge[cnt].v = v; 4 edge[cnt].w = w; 5 edge[cnt].next = head[u]; 6 head[u] = cnt++; 7 }
BFS
1 void BFS() 2 { 3 priority_queue<P>que;//优先队列 按w从小到大 4 bool vis[203];//标记数组, 标记是否被访问过 5 P p, q; 6 int i, v; 7 8 memset(vis,false,sizeof(vis));//初始化 9 p.v = s;//顶点为 s 10 p.w = 0;//距离为 0 11 que.push(p);//放入队列 12 while(que.empty() == false){//队列不为空 13 p = que.top();//取出队列的队首 14 que.pop();//删除 15 if(p.v == t){//若找到终点 16 printf("%d\n",p.w);//输出结果 17 return ;//返回 18 } 19 vis[p.v] = true;//此点标记为访问过 20 for(i=head[p.v];i!=-1;i=edge[i].next)//查找与该点相连的点 21 { 22 v = edge[i].v; 23 if(vis[v] == false){//若点未被访问过 24 q.v = v;//存入结构体 25 q.w = p.w+edge[i].w;//距离更新 26 que.push(q);//放入队列 27 } 28 } 29 } 30 printf("-1\n");//若没有到达终点 输出-1 31 }
参考资料 : 1 《算法导论》 第三版
2 《挑战程序设计》第2版
3 百度百科
4 维基百科
最短路算法 :Bellman-ford算法 & Dijkstra算法 & floyd算法 & SPFA算法 详解,布布扣,bubuko.com
最短路算法 :Bellman-ford算法 & Dijkstra算法 & floyd算法 & SPFA算法 详解
标签:style blog http color 使用 os io 数据
原文地址:http://www.cnblogs.com/Yan-C/p/3916281.html