对最小生成树和最短路径相关算法的简要总结

文章目录

  • 一、两类基本问题
    • 1.基本问题描述
    • 2.基本使用条件
  • 二、最小生成树常用算法
    • 1.Prim算法
    • 2.Kruskal算法
    • 3.延伸内容
  • 三、最短路径常用算法
    • 1.Bellman-Ford算法
    • 2.Dijkstra算法
    • 3.Floyd算法
    • 4.SPFA算法
    • 5.延伸总结
  • 四、不同算法的对比
    • 1.Dijkstra和Prim算法的异同
    • 2.不同算法复杂度的对比
  • 五、关于数据结构
    • 1.邻接矩阵
    • 2.邻接表
    • 3.链式前向星
    • 4.延伸比较

最小生成树和最短路径都是图论中比较基本的内容,我最开始在大学接触时感觉懵懵懂懂的,后来工作之后重新看算法相关的东西算是都会编代码了,但知道最近我重新把相关的内容深入学习了一遍才感觉把原理性的东西也弄明白了。当然,我所说的弄明白了只是最基本的内容。
以下只说我理解之后的干货,至于每个算法的详细及通俗介绍,网上一搜一大片,我没必要在这里重复讲了。

一、两类基本问题

1.基本问题描述

最小生成树是在连通图中找出最少的边将所有点联通,使得边的权值之和最小。所以对于有n个点的连通图,其最小生成树一定是有n-1条边。但是由于原图中边的权值可能相同,找到的最小生成树可能不是唯一的。
最短路径是给定图中两点,求最短路径。如果图是不连通的,则最短路可能不存在或者说无穷大。通常是给定起点和终点求最短路径,但一般算法都会顺带求出其他最短路径,甚至floyd算法会一次性求出所有点两两之间的最短路径。

2.基本使用条件

最小生成树要求图是连通图。连通图指图中任意两个顶点都有路径相通,通常指无向图。理论上如果图是有向、多重边的,也能求最小生成树,只是不太常见。
最短路径同样要求图是连通图,否则有些点之间就不存在路径了。通常是用于无向单重边的图。不同的算法对图中是否有负边或负权是有适用条件的,后面会再说。

二、最小生成树常用算法

1.Prim算法

基本思路
是从任取一个点作为初始集合开始,找出与集合中的点距离最小的外部点加入作为新的集合,然后不断重复直到所有点加入集合,不同点加入集合时对应的最短边的集合就构成了最小生成树。
性能分析
用V表示顶点数,E表示边数。算法的外循环最多为V-1次,如果图用邻接矩阵存储则内循环也需要O(V)次,因而基本算法复杂度为O(V^2)。
但是如果用堆结构来维护已访问过的点的集合到未访问过的点的距离时,时间复杂度可以优化到O(ElogV),这时内循环就没有了,而变成了O(E)次存取边的操作,而每次存取的时间复杂度为logV(因为用的是堆结构)。后面讲到Dijkstra最短路径算法也有类似的优化。
适用场景
通常图为稠密图时用Prim算法相比Kruskal会比较好。
而上述优化的适用条件是图为稀疏图,这时E远小于V^2。如果是稠密图,则此优化无意义。
所以稀疏图情况下,使用堆的Prim算法与Kruskal算法复杂度相当。但是由于写法上Kruskal算法更简洁方便,所以Kruskal算法更常用。
延伸讨论
用斐波那契堆能够进一步降低复杂度到O(V * lgV)。参考博文Dijkstra算法与Prim算法的异同

2.Kruskal算法

基本思路
是将所有的边排序,然后从最小的边开始,只要满足加入边不构成环,就加入集合,直到加入n-1条边,就构成了最小生成树。
性能分析
算法分为排序和主体两步。对所有边排序的复杂度为O(ElogV),至于后面最小生成树的主体部分,则是O(E)的,因为Kruskal算法通常会配并查集数据结构来用于判环,该数据结构在稳定状态下复杂度为常数级。所以Kruskal算法整体复杂度为O(ElogV)。
适用场景
更适用于稀疏图。而实际情况中图一般就是稀疏图。

3.延伸内容

两种算法的图文解释可以参考以下链接:
最小生成树(Kruskal和Prim算法)

三、最短路径常用算法

此部分基本框架参考了博文:最短路径算法。

1.Bellman-Ford算法

基本思路
对所有边进行松弛操作V-1次,即可得到单源最短路径。
关于松弛操作的深入理解请参考该博客。
性能分析
时效性较好,时间复杂度O(VE),其中外循环复杂度为O(V),内循环复杂度为O(E)。
适用场景
求单源最短路,允许有负权边,但不能有负圈。不过如果有负圈,可以判断出来,判断的方法是第n次外循环最短路径值仍然会更新。所以也可用于判断负圈(若有则不存在最短路)。
有负权边的情况下求最短路只能用Bellman-Ford算法。
延伸内容
1、Bellman-Ford算法的图文解释可参考博文:
Bellman-Ford最短路径算法
Bellman-Ford算法详解
2、对算法正确性的理解需要知道松弛操作的相关性质。可参考博文松弛操作的性质。
3、算法的外循环其实可以提前终止,条件是所有松弛操作都没有更新最短路径。参考博文最短路算法 :Bellman-ford算法 & Dijkstra算法 & floyd算法 & SPFA算法 详解。

2.Dijkstra算法

基本思路
与最小生成树Prim算法很类似,是将起点作为初始集合开始,根据集合找出与起点距离最小的外部点加入作为新的集合,然后不断重复直到目标点加入集合。
后文会讲两种算法的区别。
性能分析
算法比较稳定,的时间复杂度可为O(V^2)。用堆结构来已找到的最短距离时,时间复杂度可以优化到O(ElogV)。因而其时间复杂度跟最小生成树Prim算法非常相似。
适用场景
最常用的最短路径算法。求单源、无负权的最短路(显然也不会有负圈)。
无负权边的情况下适用,复杂度相对于Bellman-Ford算法更低。
另外据有的博客讲,其实并不是必须无负权边,有些特殊情况下有负权边也能用。
延伸内容
1、据说该算法也可以用于判断负环,参考博文关于dijkstra判断负环的思考。
2、Dijkstra算法其实是BFS(广度优先搜索)的升级版,边无权就是BFS,边有权就成了Dijkstra。
关于dfs,bfs,Dijkstra的比较可参考博文简述dfs,bfs,Dijkstra思想及区别。
3、算法的图文解释可参考博文Dijkstra算法图文详解。

3.Floyd算法

基本思路
Floyd-Warshall算法(Floyd-Warshall algorithm)是解决任意两点间的最短路径的一种算法,可以正确处理有向图或负权的最短路径问题。
求多源、无负权边的最短路。是一个动态规划算法特别经典以及简洁,只有五行:
在这里插入图片描述
性能分析
用矩阵记录图。时效性较差,时间复杂度O(V^3)。
适用场景
求多对多的最短路径时用floyd就一起求出来了。
也可以用于判断是否有负圈,方法是看是否循环完毕存在d[i][i]为负数的顶点i。
延伸内容
1、需要注意算法的写法,中间点k的遍历一定是在外循环。
2、算法的正确性理解与Bellman-Ford算法有些类似,可以参考博客floyd算法:我们真的明白floyd吗?

还可以参考《挑战程序设计竞赛》第2版P103从动态规划的思路所给的证明,但个人认为该证明并不显然,不如上一篇博文。

4.SPFA算法

相对用的少一些,虽然算法复杂度有时会更好,但据有些测试说复杂度不稳定。
其实是Bellman-Ford的队列优化,时效性相对好,时间复杂度O(kE)。(k< 与Bellman-ford算法类似,SPFA算法采用一系列的松弛操作以得到从某一个节点出发到达图中其它所有节点的最短路径。所不同的是,SPFA算法通过维护一个队列,使得一个节点的当前最短路径被更新之后没有必要立刻去更新其他的节点,从而大大减少了重复的操作次数。

5.延伸总结

求单源最短路最常用Dijkstra,如果有负权可以用Bellman-Ford,判断负圈可以用Bellman-Ford。
求多源最短路用Floyd,也可用于判负圈。
在有些特殊情况下可用SPFA,但不太常用。

四、不同算法的对比

这里把最小生成树和最短路径算法统一起来说,有一些有趣的对比。

1.Dijkstra和Prim算法的异同

Prim算法是计算最小生成树的算法,而Dijkstra算法是计算最短路径的算法,二者看起来比较类似。假设全部顶点的集合是V,已经被挑选出来的点的集合是U,那么二者都是从集合V-U中不断的挑选权值最低的点加入U,而且前面的算法复杂度分析也表明两者非常像,那么二者是否等价呢?也就是说是否Dijkstra也可以计算出最小生成树而Prim也可以计算出从第一个顶点v0到其他点的最短路径呢?答案是否定的。
那么两者的根本区别是什么呢?

(1)博文Prim和Dijkstra算法的区别解释说:
二者的不同之处在于“权值最低”的定义不同,Prim的“权值最低”是相对于U中的任意一点而言的,也就是把U中的点看成一个整体,每次寻找V-U中跟U的距离最小(也就是跟U中任意一点的距离最小)的一点加入U;而Dijkstra的“权值最低”是相对于v0而言的,也就是每次寻找V-U中跟v0的距离最小的一点加入U。
一个可以说明二者不等价的例子是有四个顶点(v0, v1, v2, v3)和四条边且边值定义为(v0, v1)=20, (v0, v2)=10, (v1, v3)=2, (v3, v2)=15的图,用Prim算法得到的最小生成树中v0跟v1是不直接相连的,也就是在最小生成树中v0v1的距离是v0->v2->v3->v1的距离是27,而用Dijkstra算法得到的v0v1的距离是20,也就是二者直接连线的长度。

(2)另一篇博文Dijkstra算法与Prim算法的异同从松弛操作的角度进行了另一种解释(本质上与前面一致):
两个伪算法的差别只在于最后循环体内的松弛操作。

  • 最小生成树只关心所有边的和最小,所以有v.key = w(u, v),即每个点直连其他点的最小值(最多只有两个节点之间的权值和)
  • 最短路径树只搜索权值最小,所以有v.key = w(u, v) + u.key,即每个点到其他点的最小值(最少是两个节之间的权值和)

这两篇博文其实说的很清楚了,Dijkstra算法与Prim算法虽然面向的是不同问题,但在思路上和复杂度上是非常相似的,唯一的核心区别是对集合外的点,是考虑到集合中点的最短距离(Prim算法),还是只考虑到起点的最短距离(Dijkstra算法)。

2.不同算法复杂度的对比

Prim和Dijkstra算法的复杂度在用二叉堆优化后都能到O(ElogV)。Kruskal算法本身的复杂度就是O(ElogV),主要耗费在排序上。
Bellman-Ford算法的复杂度是O(VE),复杂度高一些,但能解决负权和负圈问题。
Floyd算法复杂度是O(V^3),写法简单,适用于解决多对多最短路径。

五、关于数据结构

常用数据结构有三种:邻接矩阵、邻接表以及前向星。(参考博文:图的存储 邻接矩阵+邻接表+链式前向星)

1.邻接矩阵

在树的问题中,邻接矩阵是空间、时间的极大浪费。 假设树的结点个数为 N = 100000。
建立邻接矩阵需要空间为 1e5*1e5 但是由于只有 N - 1 条边,所以在邻接矩阵中只有 100000 - 1 个有效 信息。
即只利用了邻接矩阵的 0.00001%,剩余空间全部被浪费。

2.邻接表

邻接表是最常用存储结构之一。 但是 vector(动态数组) 的时间效率较低 (较普通数组而言)。
那有没有一种用普通数组可以存储, 时间和空间都极佳的存储结构?

3.链式前向星

链式前向星是介于 邻接矩阵 和 邻接表 之间比较均衡的一种数据结构。可参考博文: 对于“前向星”的理解

4.延伸比较

邻接矩阵与邻接表相比,疏松图多用邻接表,稠密图多用邻接矩阵。

另:还可参考博文图的存储结构:邻接矩阵(邻接表)&链式前向星

你可能感兴趣的:(算法分析,数据结构)