Floyed(弗洛伊德)最短路算法的证明和实现

Floyed最短路算法应该是所有最短路算法里最好理解的算法了,代码模块基本上5行就能搞定。但是我一向认为所谓的好理解有两个充分条件:1)你确实觉得理解起来比较容易,2)你确实理解了算法背后的真正原理

但是往往很多人只能做到第一点。

今天我就结合网上写的博客再加入一些自己的理解,决定剖析一下这个算法,并讲解这个算法简单表达形式背后的一些重要的原理。

在这之前,我们先阐述一些基本的“真理”:

在一共有 nn 个节点的有向无负环图中,我们把每个节点从1到 nn 进行标号,则有

  1. 图中无直接连接的两点的直接距离(边权)为 ∞\infty ,任意一点到自身的距离为0。
  2. 若节点 ii 到节点 jj ( i,ji,j 均属于 [1,n][1,n] )存在最短路径,则在其最短路径的集合中至少会有一条最短路径,在这条路径上没有重复的节点(即最短路一定不存在环。这也是为什么一开始我们定义的图是一个有向无负环图:如果有负环,那最短路可能就会包含环了,那这个岁最短路也便不存在了,因为最短路径为 −∞-\infty )

证明比较简单,大家可以简单看看。

3. [1,n][1,n] 上的任意两个节点 i,ji,j 之间必定会存在最短路径

4. a0a_{0}到 ana_{n} 的一条路径 a0⇒a1⇒a2⋅⋅⋅⋅⇒ana_{0} \Rightarrow a_{1}\Rightarrow a_{2} \cdot \cdot \cdot \cdot \Rightarrow a_{n} 为 a0a_{0} 到 ana_{n} 的最短路径的一个必要条件是对于任意 i,j(1≤i≤j≤n)i,j (1 \leq i \leq j \leq n) ,都有 ai⇒ai+1⇒ai+2⋅⋅⋅⋅⇒aja_{i} \Rightarrow a_{i+1} \Rightarrow a_{i+2} \cdot \cdot \cdot \cdot \Rightarrow a_{j} 为 aia_{i} 到 aja_{j} 的最短路径。

好啦,上面的这几条基本上就是一些基本定理的证明。

下面我们开始讲解关键部分:隐藏在Floyed背后的DP(动态规划)思想

那么怎么意识到一个问题是DP问题呢?因人而异,说一下我的思路吧。

当我看到一个问题里包含求解最大值、最小值、数量等并且此问题可以被拆解成更加小的子问题的时候,我会下意识地想到用DP来解决。

DP其实等价于递归+缓存

一般针对于一个DP问题,我们有以下的解决思路(自己学习总结的):

模仿这个思路,我们来解决一下最短路问题,我们可以容易得知属性在这里代指的是最小值(最短路径),那么集合表示的是什么呢?因为要求解 i→ji\rightarrow j 的最短路,那么从 i→ji\rightarrow j 的任意一条路径都是集合中的一个元素,那么集合显然代表的就是路径,属性是最小值,那么集合的最小值就是路径的最短路即最短路。

那么如何定义状态呢?

我们这样子想:我们的目标是求解i,ji,j 之间的最短路,从 ii 点到 jj 点一定经过了图中的其他节点 ak(k!=i,j)a_{k} (k!=i,j)(最短路也可能是直接连接 i,ji,j 的路径,即经过了0个点) , 可能经过了1个节点,也可能经过了2个,也有可能经过了 nn 个。我们不妨这样定义:记 S(k)(k≥1)S(k)(k\geq1)表示 ii 到 jj 的最短路径中只经过了标号为 1∼k1\sim k 的点, S(0)S(0) 表示没有经过任何中间点( i,ji,j 之间相互连接)。

在满足 S(m)S(m) 下,把节点 ii 到节点 jj 的最短路的长度定义为状态 d(m,i,j)d(m,i,j) (无向图中, d(m,j,i)=d(m,i,j)d(m,j,i)=d(m,i,j) ;有向图中, d(m,i,j)d(m,i,j) 不一等等于 d(m,j,i)d(m,j,i) )。而我们最终要求解的状态为 d(n,i,j)d(n,i,j) ,因此我们可以不断扩大范围从 d(0,i,j)d(0,i,j)过渡到 d(n,i,j)d(n,i,j) ,依次求解。

首先,在 S(0)S(0) 下,各点之间的最小路径长即为一开始初始用户输入得到的边权。

我们先从简单的情况开始分析: S(1)S(1)

如在下图中, d(0,1,2)=2,d(0,1,1)=0,d(0,2,1)=∞,d(0,2,4)=∞d(0,1,2)=2, d(0,1,1)=0, d(0,2,1)=\infty, d(0,2,4)=\infty

在 S(1)S(1) 下,从 ii 到 jj 有两种方法:

只允许通过1号节点时,i到j可能的路径

显然,这时 ii 到 jj 所有可能路径中的最短路为:

d(1,i,j)=min(d(0,i,j),d(0,i,1)+d(0,1,j))d(1,i,j)=min(d(0,i,j),d(0,i,1)+d(0,1,j))

我们遍历 ii 和 jj ( ii 和 jj 从1到 nn 取遍),就可以得到所有的 d(1,a1,a2)d(1,a_{1},a_{2})

在 S(2)S(2) 下,我们新加入一个节点2,根据定义我们可以知道 ii 到2的路径中只可能出现1节点,而 d(1,i,2),d(1,2,j)d(1,i,2),d(1,2,j) 在第一轮的 S(1)S(1) 下已经求得了。

同 S(1)S(1) 的情形,我们可以得到 d(2,i,j)=min(d(1,i,j),d(1,i,2)+d(1,2,j))d(2,i,j)=min(d(1,i,j),d(1,i,2)+d(1,2,j))

再遍历 i,ji,j ,就可以得到所有的 d(2,a1,a2)d(2,a_{1},a_{2})

当加入节点 mm 时,由于 ii 到 mm 的路径只可能经过标号为1~ m−1m-1 的节点,所以 ii 到 mm 的最小距离就是 d(m−1,i,m)d(m-1,i,m) ,同理, mm 到 jj 的最小距离也是 d(m−1,m,j)d(m-1,m,j)

每条路径 (不含端点)中节点的标号范围为[1,m)

在 S(m)S(m) 下, ii 到 jj 的最小路径无非就两种可能:

因此我们可以得到 d(m,i,j)d(m,i,j) 的递推公式

d(m,i,j)=min(d(m−1,i,j),d(m−1,i,m)+d(m−1,m,j))(m>1)d(m,i,j)=min(d(m-1,i,j),d(m-1,i,m)+d(m-1,m,j)) (m>1)

其中的初始条件 d(0,i,j)d(0,i,j) 由用户输入得到。

我们注意到, d(m−1,i,j)d(m-1,i,j) 仅在 S(m)S(m) 下被调用一次,但 d(m−1,i,j)d(m-1,i,j) 的值一直占据一定的内存空间,为了提高内存的效率,我们只创建一个储存体 d(i,j)d(i,j) 。

C++代码实现如下:

void floyed(){
	for(int k=0;k<n;k++){
		for(int i=0;i<n;i++){
			if(path[i][k]==INF) continue; //小的剪枝优化
			for(int j=0;j<n;j++){
			   if(path[k][j]!=INF&&path[i][j]>path[i][k]+path[k][j]){
			   	  path[i][j]=path[i][k]+path[k][j];
			   }
			}
		}
	}
}

Floyed(弗洛伊德)最短路算法的证明和实现 - 知乎 (zhihu.com)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值