一、最小生成树与最短路径树的区别
最小生成树能够保证整个拓扑图的所有路径之和最小,但不能保证任意两点之间是最短路径。
应用如网络部线,把所有的电脑(服务器?)都连起来用的网线(光纤?)最少,即用最小的代价让全村人都能上网
最短路径是从一点出发,到达目的地的路径最小。
应用如导航,两个地方怎么走距离最短。可以存在到不了的情况。
二、最小生成树
在图论中,无向图 G 的生成树(英语:Spanning Tree)是具有 G 的全部顶点,但边数最少的连通子图。[1]
一个图的生成树可能有多个。
带权图的生成树中,总权重最小的称为最小生成树。
它在实际中有什么应用呢?比如说有N个城市需要建立互联的通信网路,如何使得需要铺设的通信电缆的总长度最小呢?这就需要用到最小生成树的思想了。
求取最小生成树的算法:
2.1 Prim算法原理:
1)以某一个点开始,寻找当前该点可以访问的所有的边;
2)在已经寻找的边中发现最小边,这个边必须有一个点还没有访问过,将还没有访问的点加入我们的集合,记录添加的边;
3)寻找当前集合可以访问的所有边,重复2的过程,直到没有新的点可以加入;
4)此时由所有边构成的树即为最小生成树。
参考链接:https://wiki.jikexueyuan.com/project/step-by-step-learning-algorithm/prim-algorithm1.html
2.2 Kruskal算法原理:
现在我们假设一个图有m个节点,n条边。首先,我们需要把m个节点看成m个独立的生成树,并且把n条边按照从小到大的数据进行排列。在n条边中,我们依次取出其中的每一条边,如果发现边的两个节点分别位于两棵树上,那么把两棵树合并成为一颗树;如果树的两个节点位于同一棵树上,那么忽略这条边,继续运行。等到所有的边都遍历结束之后,如果所有的生成树可以合并成一条生成树,那么它就是我们需要寻找的最小生成树,反之则没有最小生成树。
参考链接:https://wiki.jikexueyuan.com/project/step-by-step-learning-algorithm/kruskal-algorithm.html
2.3 两算法对比
总的来说,Prim算法是以点为对象,挑选与点相连的最短边来构成最小生成树。而Kruskal算法是以边为对象,不断地加入新的不构成环路的最短边来构成最小生成树。
三、最短路径树
最短路径就是从一个指定的顶点出发,计算从该顶点出发到其他所有顶点的最短路径。
最短路径树SPT(Short Path Tree)是网络的源点到所有结点的最短路径构成的树。
常见的最短路径算法有三种:dijkstra,floyd,Bellman-Ford和SPFA。
Dijkstra:适用于权值为非负的图的单源最短路径,用斐波那契堆的复杂度O(E+VlgV)
Floyd:每对节点之间的最短路径,时间复杂度O(V^3)
BellmanFord:适用于权值有负值的图的单源最短路径,并且能够检测负圈,复杂度O(VE)
SPFA:适用于权值有负值,且没有负圈的图的单源最短路径,论文中的复杂度O(kE),k为每个节点进入Queue的次数,且k一般<=2,但此处的复杂度证明是有问题的,其实SPFA的最坏情况应该是O(VE).
如果说边权中有负数怎么定义呢?如果有负边,就要分两种情况了。
第一种情况:如果从某点出发,可以到达一个权值和为负数的环,那么这个点到其它点的最短距离就是负无穷了,很明显,如果有负环,且从某点可以到达这个负环,那么我可以无限得走这个负环,每走一次,距离就小一些,这种情况下,我们可以定义这个点到达其它点的最短距离为负无穷。
第二种情况:如果说不存在一个这样的负环,那么就和没有负权边一样了,但是还不是完全一样,接下来我们介绍的四种算法中,有的是可以处理负权边不能处理负环的,有的是可以处理负环的,有的是既不能处理负权边也不能处理负环的。
3.1 Dijkstra
- 不能适用于负权图
- 只适用于单源最短路问题
如果权重是负数,那就直接pass这个算法,在这里原因不表。
单源最短路径就是指只有一个出发点,到其它点的最短路径问题。
以上图G4为例,来对迪杰斯特拉进行算法演示(以第4个顶点D为起点)。以下B节点中23应为13。
即
算法流程:
- S1. 设置dis数组,dis[i]表示起点start到i的距离。
- S2. 从点集V中弹出一个dis值最小且未以该点为起点进行松弛操作的点。
- S3. 从该点松弛与其领接的各个点更新dis数组,返回S2,循环进行。
- 通过优先队列的操作可以优化S2,之后详细说明。
举个例子。这个例子也是洛谷的单源最短路径的模板题,请求出1到各点的最短路?
很显然你用肉眼看,1到本身是0,1到2、3、4的最短路分别为2,4,3。那dijkstra的操作流程是什么呢?
首先我们先开一个dis数组,让数组的值足够大,dis[i]=0x7fffffff,从1开始出发,令dis[1]=0,发现与1相连的有三个点234,那我们一个个进行松弛操作,比较if (dis[1]+w[i]<dis[i]),w表示各边的权重,如果小于,那就让其覆盖本身的dis值,即dis[i]=dis[1]+w[i],这一波更新完后,234的值分别为2,5,4。
然后,我们需要让234全部入队,并选取dis值最小的数即2继续进行松弛操作,发现连接的是3和4,继续更新,这波结束,234的值分别为2,4,3。
接着,是上一轮dis值次小的点4,进行操作,但是4没有出的边,所以不进行操作。
最后就是剩下的一个3了,3和4还有一条权边,但是4最小的dis值依旧是3。
下面我们发现算法到这就截止了,为什么呢,因为S2的一句话,未进行松弛的点,早在第一轮234就已经全部进入过队列并且已经弹出过了,所以之后他们也不会再进入队列,我们可以设置一个bool类型的vis[i]数组代表第i个点是否被访问过了,如果访问过了就结束此循环,或者直接不push进入队列。
这就是整个dijkstra的算法。
总结:
Dijkstra算法是一种优秀的经典的最短路算法,在优先队列的优化下,它的时间复杂度也是可接受的,它在计算机网络等应用中广泛存在,然而它也有它的局限性,它的局限性甚至比Floyd算法都要大,Floyd算法允许有负权边,不允许有负权环,Dijkstra算法连负权边都不允许,因为Dijkstra的正确性基于以下假设:当前未找到最短路的点中,距离源点最短的那个点,无法继续被松弛,这时候他肯定是已经松弛到最短状态了。但是这个假设在有负权边的时候就不成立了,举一个经典例子:一个具有三个点三条边的图,0->1距离为2,0->2距离为3,2->1距离为-2,根据Dijkstra算法,我们第一次肯定会将节点1加入到已经松弛好的点集中,但是这时候是不对的,2并不是0->1最短的路径,而通过点2松弛过的路径:0->2->1才是最短的路径,因为负权边的存在,我们认为的已经被松弛好的点,在未来还有被松弛的可能。这个就是Dijkstra的局限性,然而这并不影响它的伟大,因为在实际应用中,没有负权的图是更为常见的。但是我们仍然需要可以处理负权,甚至负环的算法,这时候Bellman-Ford算法和SPFA算法就派上用场了。
3.2 Floyd
弗洛伊德算法是一种基于动态规划的多源最短路算法。是解决任意两点间的最短路径的一种算法,可以正确处理有向图或有向图或负权(但不可存在负权回路)的最短路径问题,同时也被用于计算有向图的传递闭包。
算法流程:
通过Floyd计算图G=(V,E)中各个顶点的最短路径时,需要引入两个矩阵,矩阵S中的元素a[i][j]表示顶点i(第i个顶点)到顶点j(第j个顶点)的距离。矩阵P中的元素b[i][j],表示顶点i到顶点j经过了b[i][j]记录的值所表示的顶点。
算法描述:
a. 从任意一条单边路径开始。所有两点之间的距离是边的权,如果两点之间没有边相连,则权为无穷大。
b. 对于每一对顶点 u 和 v,看看是否存在一个顶点 w 使得从 u 到 w 再到 v 比己知的路径更短。如果是更新它。
假设图G中顶点个数为N,则需要对矩阵D和矩阵P进行N次更新。初始时,矩阵D中顶点a[i][j]的距离为顶点i到顶点j的权值;如果i和j不相邻,则a[i][j]=∞,矩阵P的值为顶点b[i][j]的j的值。 接下来开始,对矩阵D进行N次更新。第1次更新时,如果”a[i][j]的距离” > “a[i][0]+a[0][j]”(a[i][0]+a[0][j]表示”i与j之间经过第1个顶点的距离”),则更新a[i][j]为”a[i][0]+a[0][j]”,更新b[i][j]=b[i][0]。 同理,第k次更新时,如果”a[i][j]的距离” > “a[i][k-1]+a[k-1][j]”,则更新a[i][j]为”a[i][k-1]+a[k-1][j]”,b[i][j]=b[i][k-1]。更新N次之后,操作完成!
举个例子
第一步,我们先初始化两个矩阵,得到下图两个矩阵:
第二步,以v1为中介,更新两个矩阵:
发现,a[1][0]+a[0][6] < a[1][6] 和a[6][0]+a[0][1] < a[6][1],所以我们只需要矩阵D和矩阵P,结果如下:
通过矩阵P,我发现v2–v7的最短路径是:v2–v1–v7
第三步:以v2作为中介,来更新我们的两个矩阵,使用同样的原理,扫描整个矩阵,得到如下图的结果:
OK,到这里我们也就应该明白Floyd算法是如何工作的了,他每次都会选择一个中介点,然后,遍历整个矩阵,查找需要更新的值,下面还剩下五步,就不继续演示下去了,理解了方法,我们就可以写代码了。
总结
Floyd算法只能在不存在负权环的情况下使用,因为其并不能判断负权环,上面也说过,如果有负权环,那么最短路将无意义,因为我们可以不断走负权环,这样最短路径值便成为了负无穷。
但是其可以处理带负权边但是无负权环的情况。
以上就是求多源最短路的Floyd算法,基于动态规划,十分优雅。
但是其复杂度确实是不敢恭维,因为要维护一个路径矩阵,因此空间复杂度达到了O(n^ 2 ) ,时间复杂度达到了O(n^ 3),只有在数据规模很小的时候,才适合使用这个算法。
3.3 Bellman-Ford
Bellman-Ford是最常规下的单源最短路问题,对边的情况没有要求,不仅可以处理负权边,还能处理负环。可谓是来者不拒。
算法描述:
对于一个图G(v,e)(v代表点集,e代表边集),执行|v|-1次边集的松弛操作,所谓松弛操作,就是对于每个边e1(v,w),将源点到w的距离更新为:原来源点到w的距离 和 源点到v的距离加上v到w的距离 中较小的那个。v-1轮松弛操作之后,判断是否有源点能到达的负环,判断的方法就是,再执行一次边集的松弛操作,如果这一轮松弛操作,有松弛成功的边,那么就说明图中有负环。算法复杂度为O(ne),e为图的边数
优化(SPFA)
Bellman-Ford算法虽然可以处理负环,但是时间复杂度为O(ne),在图为稠密图的时候,是不可接受的。复杂度太高。我们分析一下能不能进行优化,Bellman-Ford算法的正确性保证依赖于路径松弛性质,我们只要能够保证最短路径中的边的松弛顺序即可,Bellman-Ford算法属于一种暴力的算法,即,每次将所有的边都松弛一遍,这样肯定能保证顺序,但是仔细分析不难发现,源点s到达其他的点的最短路径中的第一条边,必定是源点s与s的邻接点相连的边,因此,第一次松弛,我们只需要将这些边松弛一下即可。第二条边必定是第一次松弛的时候的邻接点与这些邻接点的邻接点相连的边(这句话是SPFA算法的关键,请读者自行体会),因此我们可以这样进行优化:设置一个队列,初始的时候将源点s放入,然后s出队,松弛s与其邻接点相连的边,将松弛成功的点放入队列中,然后再次取出队列中的点,松弛该点与该点的邻接点相连的边,如果松弛成功,看这个邻接点是否在队列中,没有则进入,有则不管,这里要说明一下,如果发现某点u的邻接点v已经在队列中,那么将点v再次放到队列中是没有意义的。因为即时你不放入队列中,点v的邻接点相连的边也会被松弛,只有松弛成功的边相连的邻接点,且这个点没有在队列中,这时候稍后对其进行松弛才有意义。因为该点已经更新,需要重新松弛。
总结:
Bellman-Ford算法和SPFA算法,都是基于松弛操作,以及路径松弛性质的,不同之处在于,前者是一种很暴力的算法,为了保证顺序,每次把所有的边都松弛一遍,复杂度很高,而后者SPFA,则是在Bellman-Ford算法的基础上进行了剪枝优化,只松弛那些可能是下一条路径的边,但是关于SPFA算法的复杂度,现在还没有个定论,不过达成的共识是:算法的时间复杂度为O(me),其中m为入队的平均次数,其发明者,西安交大的段凡丁大神的论文中,m是一个常数(这个结论是错的),我们“姑且”认为该算法的时间复杂度为O(e)【滑稽】,的确该算法的效率是很高的,博主在oi中经常使用这个算法,除非题目数据特意卡SPFA。或者的确是稠密图,一般情况下不会超时。关于其判断负环,我们可以认为,在某个节点入队次数达到n的时候,就可以说明遇到了负环。SPFA有两个优化策略:SLF:Small Label First 策略,设要加入队列的节点是j,队首元素为i,若d[j]<dist[i],则将j插入队首,否则插入队尾; LLL:Large Label Last 策略,设队首元素为i,队列中所有d值的平均值为x,若d[i]>x则将i插入到队尾,查找下一元素,直到找到某一i使d[i]<=x,则将i出队进行松弛操作。其实,实际应用中,SPFA的时间复杂度是很不稳定的,因此我们能用Dijkstra+优先队列,就用Dijkstra+优先队列为好。
https://zhuanlan.zhihu.com/p/34922624
https://zhuanlan.zhihu.com/p/150434472
https://zhuanlan.zhihu.com/p/33162490
https://zhuanlan.zhihu.com/p/40338107