【图论】最短路
前言
最短路是图论最重要的算法之一,也是算法难点。经过这篇的学习,你会发现你离成功就差一个最短路的距离(虽然它难以被找到)。最短路是指,某个点到某个点之间的距离最短的路径。
必要的知识
学会存储一张有向图,我们一般使用邻接矩阵和邻接表。因为邻接矩阵的储存方式有时无法满足题目规定的空间复杂度限制,为此这里我们只使用邻接表储存有向图。储存无向图可以看成是两条相反方向的有向边
关于邻接表的存图方式,请浏览【数据结构】邻接表与链式前向星 。
2023/6/16修正:
此存图方式应为链式前向星
,邻接表使用vector
而非数组模拟链表,详见上方链接
最短路算法
一、Dijkstra算法
1.算法基本介绍
Dijkstra算法通常是求解单源最短路中最快的算法,但它无法处理存在负权边的情况(原因在正确性证明中)。Dijkstra本质上是一种贪心算法,通过不断调整每个点的“当前距离”最终得到最优结果,其实后面要讲到的几种算法也大都是采用这种步步逼近的手段。这种不断调整的过程,维基百科上面称为“relax”。以上可能有点抽象,下面是算法的流程。
2.算法流程
假设现在要求出从某一点s到其他所有点的最短距离,对于每个点v均维护一个“当前距离”()和“是否访问过”()。首先将初始化为0,将其他点的距离初始化为无穷,并将所有点初始化为未访问的。记的边权为(程序中我们使用edge数组存储,下标为这条边的序号,详见邻接表一栏)。然后进行以下步骤:
- 从所有未访问的点中,找出当前距离最小的,设为u,并将其标记为已访问的。
- 调整u的所有边(若是有向图则为出边)连接的并且未被访问过的点:若, 则将更新为。
- 重复1和2步骤,直到所有点都被标记为已访问的,则即s到i的最短距离。如果只想求从s到某一点的最短距离,那么当该点被标记为访问过之后可直接退出。
- 补充:如果除了最短距离之外还想求出具体的路径,只需建立一个数组,在步骤2后添加操作:(前提是被更新)。类似于邻接表中son数组的链式结构。
3.正确性证明
我们证明,当某个点v被标记为已访问后,s到其的最短距离即为,即在步骤1中选出的每个点的当前距离即为最短距离。
首先容易理解,如果从s到v的路径只允许经过已访问过的点,那么是这些路径中最短的。倘若存在一条包含未访问过点(除v之外)的更短的路径,设这条路径上第一个未被访问过的点为m,则显然有(注意因为m是这条路径上第一个未被访问的点,所以沿着这条路径走到m的距离一定为)。而这与v的选取方式——从所有未访问过点中选取当前距离最小的相矛盾。
解释一下Dijkstra算法为什么不能用于有负权的边的图:
因为Dijkstra算法是通过当前离起点最近的点来更新其他的点的距离,例如上图中的 4 号结点会被 2 号结点更新为2+3=5,但实际上4号结点的最短路径是4+(-4)=0!,这样你就知道为什么Dijkstra算法不能用于有负权的边的图吧。
4.代码实现
这里使用邻接表储存有向图,关于邻接矩阵的算法读者可以自己上网查询。
1 |
|
堆优化Dijkstra:
我们使用小根堆对dist数组进行维护,每次只扩展有可能改变的结点,我们只需要的时间即可维护一个二叉堆,而找到最小结点只需要。
关于小根堆的实现方法读者可自行查询,这里我们直接存储负值。
1 |
|
二、Bellmon-Ford 算法和 SPFA
首先介绍bellmon-ford算法,SPFA(shortest path faster algorithm)是对它的一个改进。
bellmon-ford是一种单源最短路算法,时间复杂度是 ,显然不如Dijkstra快,但它可以处理负权边和负权环的情况。它基于一个很基本的事实: 对于一个不包含负权环的V个点的图,任意两点之间的最短路径至多包含条边。 如果存在负权环,每次在负权环上走一圈都会使环上的每一个点的距离减少 (如果计算时步数超过了,说明有负权环),因此不存在最短路径。bellmon-ford算法可以检测出这种情况。
算法的实现也很简单,根据三角形不等式,对于图中的某一条边,有成立,就说它满足三角形不等式,可以发现,此不等式就是这一条边在有限范围内的最优解。通过重复扫描边,直到所有边都符合三角不等式,此时dist数组就是结果(是不是很暴力很简单)。
正因为它不像Dijkstra这么贪心,不会漏过负权边,所以它可以正确处理有负权边的图的最短路问题。
1 |
|
此版本的Bellmon-Ford算法不需要邻接表,但SPFA仍然要使用邻接表,望读者一定要熟悉邻接表的使用方法,不要被暴力迷了双眼
算法中,为源点到i的当前最短距离。算法最外层是一个次的循环,在每次循环中,算法遍历所有的边并执行操作。
bellmon-ford算法不断对每条边进行所谓的松弛(relax)操作,如果u到v有一条边,那么减小意味着可能也可以进行更新,然而由于不知道到底哪些点的当前距离需要更新,bellmon-ford算法选择暴力地去遍历每条边来检查哪些点的当前距离可以更新 (这也是我们要优化的地方)。
如果在某次外层循环中发现所有点的当前距离都没有被更新,那么可以直接停止算法,因为接下来无论再进行多少次循环点的距离也不会被继续更新了。
bellmon-ford算法可以判断负权环的存在:只需在算法的最后对每条边再松弛一次,如果发现有点的距离得到更新 (说明经过次循环仍然有边不满足三角形不等式,而只有负环会出现这种情况,因为负环永远不可能满足三角形不等式),说明存在负权环——因为没有负权环时最短路径的长度至多为。
SPFA:
从上面的介绍我们知道bellmon-ford算法是带着一定的盲目性的,因为三角形不等式是这一条边在有限范围内的最优解,很多时候(比如刚开始赋值无限大)根本不存在最优解,白白浪费了时间。作为对它的优化,SPFA采用类似BFS的思想,使用一个队列,只松弛那些可能更新点的距离的边,也就是那些已经被改变,不再是无限大(也可能再次被改变),可能对下一个结点有影响的结点队列。算法的流程为:
- 将除源点之外的所有的点当前距离初始化为无穷,并标记为未入队。源点的当前距离为0,将源点入队。
- 取出队首u,遍历u的所有出边,检查是否能更新所连接的点v的当前距离。如果v的当前距离被更新并且v不在队中,则将v入队。重复该操作直到队列为空。
- 检查是否存在负权环的方法为:记录一个点的入队次数,如果超过次说明存在负权环,因为最短路径上除自身外至多个点,故一个点不可能被更新超过次。
1 |
|
三、Floyd算法
Floyd算法又称为Floyd-Warshell算法,其实Warshell算法是离散数学中求传递闭包的算法,两者的思想是一致的。Floyd算法是求解多源最短路时通常选用的算法,经过一次算法即可求出任意两点之间的最短距离,并且可以处理有负权边的情况(但无法处理负权环)。
原理:
Floyd本质上是动态规划的思想。倘若现在我们想求i到j的最短路径长度,我们限制这条路径上除i和j之外只准经过前k个点(这样的路径称为k允许路径),我们在算法的最外层循环每次将k加1,那么当k等于点数时求得的结果便是最优的。下面用数学归纳法证明算法的正确性:
即证明在第n次循环后dist[i][j]为i到j的最短n允许路径,当n=0时,i到j不准经过任何点,dist[i][j]即为weight[i->j]或者无穷大,显然成立。
假设n=k时成立,那么当n=k+1时,倘若i到j的最短k+1允许路径不经过第k+1个点,则dist[i][j]不发生改变。若i到j的最短k+1允许路径经过第k+1个点,由归纳假设,此时dist[i][k+1]为i到k+1的最短k允许路径,dist[k+1][j]为k+1到j的最短k允许路径,故i到j的最短k+1允许路径长为dist[i][k+1] + dist[k+1][j]。这正是算法中的状态转移方程。
算法的实现非常简单,是一个三重循环:
1 | int dist[100][100]; //初始化dist数组 |
- 标题: 【图论】最短路
- 作者: EveSunMaple
- 创建于 : 2023-05-14 00:05:00
- 更新于 : 2024-02-23 12:02:20
- 链接: https://old.saroprock.com/post/1824baaa.html
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。