经典最短路径算法学习笔记
前言
最短路径的概念
在一个图中,从一个顶点 到另一个顶点 存在着几条路径。路径是一个顶点到另一个顶点所经过的边的集合。路径的长度为所经过的边的权值之和。
- 在不带权图中(图中的边没有值),默认一个边值为1,即每条边的带权值为1,这样可以算出每条路径的长度,取其中的长度值最小的为这两个点的最短路径。
- 在带权图中,同样,计算出每条路径的长度,取其中最短的为两点间的最短路径。
note:一般的,如果两个点之间没有路径,即不可达,则这两个点的最短路径的长度可以设为无穷大。
单源最短路径的概念
单源最短路径,就是只有一个源点,求该源点到其他各点的最短路径。通俗的说,就是求出从一个顶点到其余顶点的最短路径。
解决该类问题的经典算法就是Dijkstra算法。想了解Dijkstra这个计算机科学家的可自行百度、谷歌~
Dijkstra算法的限制:该算法只能解决各边权值大于0的最短路径。
多源最短路径的概念
多源最短路径,对照单源最短路径,显而易见的,这边的源点是多个,求每个源点到其他各点的最短路径。通俗的说,就是求每对顶点之间的最短路径。
显然的,Dijkstra算法也可以用来解决该问题,只不过需要在求解完一个源点后,再让其他的点来当源点再次求解,重复执行,最终也可以得到所有的解。
除此之外,Floyd算法也可以用来解决该问题。
单源最短路径——Dijkstra算法
首先,从大的方面上讲,Dijkstra算法用到的思想是贪心思想。其中,贪心选择方法为每一步,选择离当前点最近的点作为最短路径中经过的点。(这个选择方法是我自己总结的,emm可能说的不怎么样,但我尽力了QAQ)
Dikjstra算法的流程:
- 首先,定义一个数组dist来保存源点
s
到其他点的距离,定义一个集合visited记录访问过的节点,在这个visited集合中任意一点x
都是已经求得从s
到x
的最短路径。一开始,visited中只有源点s
;dist中的初始值为以下两种选择:- 如果
s
可以到v
点,那么$dist[v] = length[s,v] $(边sv的权值) - 否则,那么\(dist[v] = \infty\);
- 如果
- 每一次确定了一个点
v
的最短路径,就把v
放入visited集合中,然后对于未访问过的任意节点x
,选出其中dist[x]最小的点,这个点dist[x]为源点s
到x
的最短路径的长度。然后以此x
为中间点,更新dist数组。更新操作为:- 对于节点
u
,\(dist[u] = Min(dist[u], dist[x]+length[x,u])\);
- 对于节点
- 循环执行第二步,直到visited中包含图中所有的点。
算法正确性证明:
友情提示:这个证明是我在看了维基百科和数据结构书再加上自己的思考后整理出来的,准确性不敢保证 O.O
在证明这个正确性之前,先证明下面这个。
命题如下:
若从源点
v
到某个顶点j
的最短路径是\((v,...,a,...u,j)\),也就是说,在源点v
->j
的最短路径上顶点j
的前一个顶点是u
,那么其中的\((v,...,a,...u)\)一定是源点v
->u
的最短路径。
求证:
反证法证明。
假设\((v,...,a,...,u,j)\)是源点
v
到顶点j
的最短路径,但\((v,...,a,...,u)\)不是源点v
->u
最短路径。由于\((v,...,a,...,u)\)不是源点v
->u
的最短路径,设源点v
->u
的最短路径为\((v,...,b,...,u)\),如下图所示,则\((v,...,b,...,u,j)\)是一条比\((v,...,a,...,u,j)\)更短的新路径,与前面的假设矛盾,命题得证。
现在证明Dijkstra算法正确性。
假设:任意一个已访问过的点v
,若在未访问的点中,dist[u]的值最小。并且,在形成dist[u]长度的路径上u
的前一个点为v
。所以,\(dist[u] = dist[v]+length[v,u]\);
按照Dijkstra算法,此时源点s
到u
的最短路径已经确定,且值为dist[u];
依旧采用反正法证明:
若现在存在一条更短的路径到u
,且u
的前一个点为w
。
w
有两种情况:
- 该点为未访问节点,所以\(dist[w] > dist[u]\);
- 该点为已访问节点。
对于第一种情况:
对于第二种情况:
故,通过反证法证明得Dijkstra算法正确。
note:
- Dijkstra算法,若现在已选
v
点,下一个点不是选距离v
最近的点u
,而是选dist[u]最小的点u
。 - 在维基百科上看到,Dijkstra算法的原始版本是找出两点之间的最短路径。
贴个代码:
1 | /** |
测试的结果:
1 | 0 4 5 6 10 9 16 |
多源最短路径——Floyd算法
Floyd算法的流程
- 假定图graph的顶点的索引编号从1到N,二维数组dp用来保存答案,
dp[i][j]
表示顶点i
到顶点j
的最短路径长度。初始时,dp[i][j]
的值为:i
与j
相连,\(dp[i][j] = length[i,j]\);(边ij
的权值)i
与j
不相连,\(dp[i][j] = \infty\);
- 对于\(\forall k \in (1,N),\forall i \in (1,N), \forall j \in (1,N)\), 进行\(dp[i][j] = min(dp[i][j],dp[i][k]+dp[k][j])\)迭代。
- 三层循环后,返回dp数组即为所有点之间的最短路径。
算法的理解
通过上述的流程可以看出,Floyd算法的思想是动态规划。
定义:\(f(i,j,k)\)为顶点i
只能经过\((1,2,...,k)\)这些作为中间点的情况下,到顶点j
的最短路径。
也就说,当\(k = N\)时,\(f(i,j,N)\)后的\(dp[i][j]\)就为\(i \to j\)的最短路径长度。
递推式推导
对于每一对顶点,\(f(i,j,k)\)可能会产生的情况:
- \(i \to j\) 不会经过
k
,那么 \(f(i,j,k) = f(i,j,k-1)\);因为不会经过k
,所以跟之前的k-1
是一样的,此时,这里的\(f(i,j,k-1)\)是前一个状态,也就是说只要保存前一个状态下的所有对顶点的最短路径即可。 - $i j $ 会经过
k
,那么\(f(i,j,k) = f(i,k,k-1)+f(k,j,k-1)\);因为会经过k
,那就说明\(i \to k \to j\)是可以的,所以\(f(i,j,k)\)可以表示成\(f(i,k,k-1)+f(k,j,k-1)\)。在这里的\(f(i,k,k-1)\)和\(f(k,j,k-1)\)也是前一个状态。
综上,\(f(i,j,k) = min(f(i,j,k-1),f(i,k,k-1)+f(k,j,k-1))\);
理解
若定义二维数组dp来保存所有对顶点的最短路径的长度,那么在上述的推导下,迭代公式即为:
\(dp[i][j] = min(dp[i][j],dp[i][k]+dp[k][j])\)
其中,在等式的右边\(dp[i][j],dp[i][k],dp[k][j]\)都为前一个状态下的产物。
贴个代码:
1 | /** |
测试的结果:
1 | 0 4 5 6 10 9 16 |
参考文献
- https://en.wikipedia.org/wiki/Floyd%E2%80%93Warshall_algorithm
- https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm
如有不正确的地方,欢迎指正( •̀ ω •́ )✧