彻底弄懂最短路径问题.docx
- 文档编号:4448338
- 上传时间:2022-12-01
- 格式:DOCX
- 页数:11
- 大小:447.68KB
彻底弄懂最短路径问题.docx
《彻底弄懂最短路径问题.docx》由会员分享,可在线阅读,更多相关《彻底弄懂最短路径问题.docx(11页珍藏版)》请在冰豆网上搜索。
彻底弄懂最短路径问题
彻底弄懂最短路径问题
只想说:
温故而知新,可以为师矣。
我大二的《数据结构》是由申老师讲的,那时候不怎么明白,估计太理论化了(ps:
或许是因为我睡觉了);今天把老王的2011年课件又看了一遍,给大二的孩子们又讲了一遍,随手谷歌了N多资料,算是彻底搞懂了最短路径问题。
请读者尽情享用……
我坚信:
没有不好的学生,只有垃圾的教育。
不过没有人理所当然的对你好,所以要学会感恩。
一.问题引入
问题:
从某顶点出发,沿图的边到达另一顶点所经过的路径中,各边上权值之和最小的一条路径——最短路径。
解决最短路的问题有以下算法,Dijkstra算法,Bellman-Ford算法,Floyd算法和SPFA算法,另外还有著名的启发式搜索算法A*,不过A*准备单独出一篇,其中Floyd算法可以求解任意两点间的最短路径的长度。
笔者认为任意一个最短路算法都是基于这样一个事实:
从任意节点A到任意节点B的最短路径不外乎2种可能,1是直接从A到B,2是从A经过若干个节点到B。
二.Dijkstra算法
该算法在《数据结构》课本里是以贪心的形式讲解的,不过在《运筹学》教材里被编排在动态规划章节,建议读者两篇都看看。
观察右边表格发现除最后一个节点外其他均已经求出最短路径。
(1) 迪杰斯特拉(Dijkstra)算法按路径长度(看下面表格的最后一行,就是next点)递增次序产生最短路径。
先把V分成两组:
∙S:
已求出最短路径的顶点的集合
∙V-S=T:
尚未确定最短路径的顶点集合
将T中顶点按最短路径递增的次序加入到S中,依据:
可以证明V0到T中顶点Vk的最短路径,或是从V0到Vk的直接路径的权值或是从V0经S中顶点到Vk的路径权值之和(反证法可证,说实话,真不明白哦)。
(2) 求最短路径步骤
1.初使时令S={V0},T={其余顶点},T中顶点对应的距离值, 若存在
2.从T中选取一个其距离值为最小的顶点W(贪心体现在此处),加入S(注意不是直接从S集合中选取,理解这个对于理解vis数组的作用至关重要),对T中顶点的距离值进行修改:
若加进W作中间顶点,从V0到Vi的距离值比不加W的路径要短,则修改此距离值(上面两个并列for循环,使用最小点更新)。
3.重复上述步骤,直到S中包含所有顶点,即S=V为止(说明最外层是除起点外的遍历)。
下面是上图的求解过程,按列来看,第一列是初始化过程,最后一行是每次求得的next点。
(3) 问题:
Dijkstar能否处理负权边?
(来自《图论》)
答案是不能,这与贪心选择性质有关(ps:
貌似还是动态规划啊,晕了),每次都找一个距源点最近的点(dmin),然后将该距离定为这个点到源点的最短路径;但如果存在负权边,那就有可能先通过并不是距源点最近的一个次优点(dmin'),再通过这个负权边L(L<0),使得路径之和更小(dmin'+L 比如n=3,邻接矩阵: 0,3,4 3,0,-2 4,-2,0,用dijkstra求得d[1,2]=3,事实上d[1,2]=2,就是通过了1-3-2使得路径减小。 不知道讲得清楚不清楚。 二.Floyd算法 参考了南阳理工牛帅(目前在新浪)的博客。 Floyd算法的基本思想如下: 从任意节点A到任意节点B的最短路径不外乎2种可能,1是直接从A到B,2是从A经过若干个节点到B,所以,我们假设dist(AB)为节点A到节点B的最短路径的距离,对于每一个节点K,我们检查dist(AK)+dist(KB) 很简单吧,代码看起来可能像下面这样: for(inti=0;i for(intj=0;j for(intk=0;k if(dist[i][k]+dist[k][j] dist[i][j]=dist[i][k]+dist[k][j]; } } } } 但是这里我们要注意循环的嵌套顺序,如果把检查所有节点K放在最内层,那么结果将是不正确的,为什么呢? 因为这样便过早的把i到j的最短路径确定下来了,而当后面存在更短的路径时,已经不再会更新了。 让我们来看一个例子,看下图: 图中红色的数字代表边的权重。 如果我们在最内层检查所有节点K,那么对于A->B,我们只能发现一条路径,就是A->B,路径距离为9,而这显然是不正确的,真实的最短路径是A->D->C->B,路径距离为6。 造成错误的原因就是我们把检查所有节点K放在最内层,造成过早的把A到B的最短路径确定下来了,当确定A->B的最短路径时dist(AC)尚未被计算。 所以,我们需要改写循环顺序,如下: ps: 个人觉得,这和银行家算法判断安全状态(每种资源去测试所有线程),树状数组更新(更新所有相关项)一样的思想。 for(intk=0;k for(inti=0;i for(intj=0;j /* 实际中为防止溢出,往往需要选判断dist[i][k]和dist[k][j 都不是Inf,只要一个是Inf,那么就肯定不必更新。 */ if(dist[i][k]+dist[k][j] dist[i][j]=dist[i][k]+dist[k][j]; } } } } 如果还是看不懂,那就用草稿纸模拟一遍,之后你就会豁然开朗。 半个小时足矣(早知道的话会节省很多个半小时了。 。 ) 再来看路径保存问题: voidfloyd(){ for(inti=1;i<=n;i++){ for(intj=1;j<=n;j++){ if(map[i][j]==Inf){ path[i][j]=-1;//表示i->j不通 }else{ path[i][j]=i;//表示i->j前驱为i } } } for(intk=1;k<=n;k++){ for(inti=1;i<=n;i++){ for(intj=1;j<=n;j++){ if(! (dist[i][k]==Inf||dist[k][j]==Inf)&&dist[i][j]>dist[i][k]+dist[k][j]){ dist[i][j]=dist[i][k]+dist[k][j]; path[i][k]=i; path[i][j]=path[k][j]; } } } } } voidprintPath(intfrom,intto){ /* *这是倒序输出,若想正序可放入栈中,然后输出。 * *这样的输出为什么正确呢? 个人认为用到了最优子结构性质, *即最短路径的子路径仍然是最短路径 */ while(path[from][to]! =from){ System.out.print(path[from][to]+""); to=path[from][to]; } } 《数据结构》课本上的那种方式我现在还是不想看,看着不舒服…… Floyd算法另一种理解DP,为理论爱好者准备的,上面这个形式的算法其实是Floyd算法的精简版,而真正的Floyd算法是一种基于DP(DynamicProgramming)的最短路径算法。 设图G中n个顶点的编号为1到n。 令c[i,j,k]表示从i到j的最短路径的长度,其中k表示该路径中的最大顶点,也就是说c[i,j,k]这条最短路径所通过的中间顶点最大不超过k。 因此,如果G中包含边,则c[i,j,0]=边的长度;若i=j,则c[i,j,0]=0;如果G中不包含边,则c(i,j,0)=+∞。 c[i,j,n]则是从i到j的最短路径的长度。 对于任意的k>0,通过分析可以得到: 中间顶点不超过k的i到j的最短路径有两种可能: 该路径含或不含中间顶点k。 若不含,则该路径长度应为c[i,j,k-1],否则长度为c[i,k,k-1]+c[k,j,k-1]。 c[i,j,k]可取两者中的最小值。 状态转移方程: c[i,j,k]=min{c[i,j,k-1],c[i,k,k-1]+c[k,j,k-1]},k>0。 这样,问题便具有了最优子结构性质,可以用动态规划方法来求解。 看另一个DP(直接引用王老师课件) 说了这么多,相信读者已经跃跃欲试了,咱们看一道例题,以ZOJ1092为例: 给你一组国家和国家间的部分货币汇率兑换表,问你是否存在一种方式,从一种货币出发,经过一系列的货币兑换,最后返回该货币时大于出发时的数值(ps: 这就是所谓的投机倒把吧),下面是一组输入。 3 //国家数 USDollar //国家名 BritishPound FrenchFranc 3 //货币兑换数 USDollar0.5BritishPound //部分货币汇率兑换表 BritishPound10.0FrenchFranc FrenchFranc0.21USDollar 月赛做的题,不过当时用的思路是求强连通分量(ps: 明明说的,那时我和华杰感觉好有道理),也没做出来,现在知道了直接floyd算法就ok了。 思路分析: 输入的时候可以采用Map 下面就是floyd算法啦,初始化其它点为-1,对角线为1,采用乘法更新求最大值。 三.Bellman-Ford算法 为了能够求解边上带有负值的单源最短路径问题,Bellman(贝尔曼,动态规划提出者)和Ford(福特)提出了从源点逐次绕过其他顶点,以缩短到达终点的最短路径长度的方法。 Bellman-ford算法是求含负权图的单源最短路径算法,效率很低,但代码很容易写。 即进行不停地松弛,每次松弛把每条边都更新一下,若n-1次松弛后还能更新,则说明图中有负环,无法得出结果,否则就成功完成。 Bellman-ford算法有一个小优化: 每次松弛先设一个flag,初值为FALSE,若有边更新则赋值为TRUE,最终如果还是FALSE则直接成功退出。 Bellman-ford算法浪费了许多时间做无必要的松弛,所以SPFA算法用队列进行了优化,效果十分显著,高效难以想象。 SPFA还有SLF,LLL,滚动数组等优化。 关于SPFA,请看我这一篇 递推公式(求顶点u到源点v的最短路径): dist1[u]=Edge[v][u] distk[u]=min{distk-1[u],min{distk-1[j]+Edge[j][u]}},j=0,1,…,n-1,j≠u Dijkstra算法和Bellman算法思想有很大的区别: Dijkstra算法在求解过程中,源点到集合S内各顶点的最短路径一旦求出,则之后不变了,修改 的仅仅是源点到T集合中各顶点的最短路径长度。 Bellman算法在求解过程中,每次循环都要修改所有顶点的dist[],也就是说源点到各顶点最短路径长度一直要到Bellman算法结束才确定下来。 算法适用条件 ∙1.单源最短路径(从源点s到其它所有顶点v) ∙有向图&无向图(无向图可以看作(u,v),(v,u)同属于边集E的有向图) ∙边权可正可负(如有负权回路输出错误提示) ∙差分约束系统(至今貌似只看过一道题) Bellman-Ford算法描述: 1.初始化: 将除源点外的所有顶点的最短距离估计值d[v]←+∞,d[s]←0 2.迭代求解: 反复对边集E中的每条边进行松弛操作,使得顶点集V中的每个顶点v的最短距离估计值逐步逼近其最短距离;(运行|v|-1次,看下面的描述性证明(当做树)) 3.检验负权回路: 判断边集E中的每一条边的两个端点是否收敛。 如果存在未收敛的顶点,则算法返回false,表明问题无解;否则算法返回true,并且从源点可达的顶点v的最短距离保存在d[v]中 描述性证明: (这个解释很好) 首先指出,图的任意一条最短路径既不能包含负权回路,也不会包含正权回路,因此它最多包含|v|-1条边。 其次,从源点s可达的所有顶点如果存在最短路径,则这些最短路径构成一个以s为根的最短路径树。 Bellman-Ford算法的迭代松弛操作,实际上就是按顶点距离s的层次,逐层生成这棵最短路径树的过程。 在对每条边进行1遍松弛的时候,生成了从s出发,层次至多为1的那些树枝。 也就是说,找到了与s至多有1条边相联的那些顶点的最短路径;对每条边进行第2遍松弛的时候,生成了第2层次的树枝,就是说找到了经过2条边相连的那些顶点的最短路径……。 因为最短路径最多只包含|v|-1条边,所以,只需要循环|v|-1次。 每实施一次松弛操作,最短路径树上就会有一层顶点达到其最短距离,此后这层顶点的最短距离值就会一直保持不变,不再受后续松弛操作的影响。 (但是,每次还要判断松弛,这里浪费了大量的时间,这就是Bellman-Ford算法效率底下的原因,也正是SPFA优化的所在)。 ,如图(没找到画图工具的射线),若是B和C的最短路径不更新,那么点D的最短路径肯定也无法更新,这就是优化所在。 如果没有负权回路,由于最短路径树的高度最多只能是|v|-1,所以最多经过|v|-1遍松弛操作后,所有从s可达的顶点必将求出最短距离。 如果d[v]仍保持+∞,则表明从s到v不可达。 如果有负权回路,那么第|v|-1遍松弛操作仍然会成功,这时,负权回路上的顶点不会收敛。 参考了《图论》。 问题: Bellman-Ford算法是否一定要循环n-1次么? 未必! 其实只要在某次循环过程中,考虑每条边后,都没能改变当前源点到所有顶点的最短路径长度,那么Bellman-Ford算法就可以提前结束了(开篇提出的小优化就是这个)。 上代码(参考了牛帅的博客) #include #include usingnamespacestd; #defineMAX0x3f3f3f3f #defineN1010 intnodenum,edgenum,original;//点,边,起点 typedefstructEdge//边 { intu,v; intcost; }Edge; Edgeedge[N]; intdis[N],pre[N]; boolBellman_Ford() { for(inti=1;i<=nodenum;++i)//初始化 dis[i]=(i==original? 0: MAX); for(inti=1;i<=nodenum-1;++i) for(intj=1;j<=edgenum;++j) if(dis[edge[j].v]>dis[edge[j].u]+edge[j].cost)//松弛(顺序一定不能反~) { dis[edge[j].v]=dis[edge[j].u]+edge[j].cost; pre[edge[j].v]=edge[j].u; } boolflag=1;//判断是否含有负权回路 for(inti=1;i<=edgenum;++i) if(dis[edge[i].v]>dis[edge[i].u]+edge[i].cost) { flag=0; break; } returnflag; } voidprint_path(introot)//打印最短路的路径(反向) { while(root! =pre[root])//前驱 { printf("%d-->",root); root=pre[root]; } if(root==pre[root]) printf("%d\n",root); } intmain() { scanf("%d%d%d",&nodenum,&edgenum,&original); pre[original]=original; for(inti=1;i<=edgenum;++i) { scanf("%d%d%d",&edge[i].u,&edge[i].v,&edge[i].cost); } if(Bellman_Ford()) for(inti=1;i<=nodenum;++i)//每个点最短路 { printf("%d\n",dis[i]); printf("Path: "); print_path(i); } else printf("havenegativecircle\n"); return0; } 四.SPFA算法 用一个队列来进行维护。 初始时将源加入队列。 每次从队列中取出一个元素,并对所有与他相邻的点进行松弛,若某个相邻的点松弛成功,则将其入队。 直到队列为空时算法结束;这个算法,简单的说就是队列优化的bellman-ford,利用了每个点不会更新次数太多的特点发明的此算法(看我上面那个图,只有相邻点更新了,该点才有可能更新)。 代码参见 : 五.趣闻 整理该篇博文的时候,一哥们发布网站到我们群,网站很精美,一牛神(acmol)使用fork炸弹,结果服务器立马挂啦,更改后又挂啦,目测目前无限挂中。 。 。 作者: 张朋飞 出处: 本文版权归作者张朋飞所有,欢迎转载和商用,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利.
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 彻底 弄懂 路径 问题