最短路径算法——Floyd算法详解

代码

Floyd算法代码非常简洁,它是一种基于动态规划的多源最短路径算法(即可以求出每个点对之间的最短路径)。
初步分析来看,代码就是个三层嵌套循环,然后就是k层循环在最外层。其实记住这些,就够使用了,但研究算法还是不求甚解比较好。

    private void floyd() {
        for (int k = 0; k < n; k++) {
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < n; j++) {
                    a[i][j] = Math.min(a[i][j], a[i][k] + a[k][j]);
                }
            }
        }

原理分析

假设节点序号是从1到n。
假设 f [ 0 ] [ i ] [ j ] 是一个n*n的矩阵,第i行第j列代表从i到j的权值,如果i到j有边,那么其值就为 c i , j (边ij的权值)。如果没有边,那么其值就为无穷大。

f [ k ] [ i ] [ j ] 代表(k的取值范围是从1到n),在考虑了从1到k的节点作为中间经过的节点时,从i到j的最短路径的长度。

比如, f [ 1 ] [ i ] [ j ] 就代表了,在考虑了1节点作为中间经过的节点时,从i到j的最短路径的长度。

分析可知, f [ 1 ] [ i ] [ j ] 的值无非就是两种情况,而现在需要分析的路径也无非两种情况, i => j i => 1 => j
【1】 f [ 0 ] [ i ] [ j ] i => j 这种路径的长度,小于, i => 1 => j 这种路径的长度
【2】 f [ 0 ] [ i ] [ 1 ] + f [ 0 ] [ 1 ] [ j ] i => 1 => j 这种路径的长度,小于, i => j 这种路径的长度

形式化说明如下:
f [ k ] [ i ] [ j ] 可以从两种情况转移而来:
【1】从 f [ k 1 ] [ i ] [ j ] 转移而来,表示i到j的最短路径不经过k这个节点
【2】从 f [ k 1 ] [ i ] [ k ] + f [ k 1 ] [ k ] [ j ] 转移而来,表示i到j的最短路径经过k这个节点

总结就是: f [ k ] [ i ] [ j ] = m i n ( f [ k 1 ] [ i ] [ j ] , f [ k 1 ] [ i ] [ k ] + f [ k 1 ] [ k ] [ j ] )
从总结上来看,发现 f [ k ] 只可能与 f [ k 1 ] 有关。

动态规划

从动态规划的角度分析,题目需要合理的定义状态,划分阶段

我们定义 f [ k ] [ i ] [ j ] 为考虑从1到k这些节点后,所能得到的最短路径的长度。
f [ k ] [ i ] [ j ] 可以从 f [ k 1 ] [ i ] [ j ] 转移而来,表示 i j 的最短路径不经过 k 这个节点。
f [ k ] [ i ] [ j ] 也可以从 f [ k 1 ] [ i ] [ k ] + f [ k 1 ] [ k ] [ j ] 转移而来,表示 i j 的最短路径经过 k 这个节点。

考虑以上状态的定义,是否满足最优子结构无后效性原则。
最优子结构:在图中,显而易见,最短路径的子路径仍然是最短路径。比如一条从1到5的最短路径为1=>2=>3=>4=>5,那么1=>2=>3一定是1到3的最短路径,3=>4=>5一定是3到5的最短路径。形式化说明则是,从j到j的最短路径一定是由从i到k的最短路径再合上从k到j的最短路径组成的。

上面这个例子虽然方便理解,但如果要结合Floyd算法和上述例子分析的话,上述例子是把一个有4截的最短路径,分为有2截的最短路径加起来,那么Floyd算法的k层循环则是每次只加1截最短路径。

所谓无后效性原则,指的是这样一种性质:某阶段的状态一旦确定,则此后过程的演变不再受此前各状态及决策的影响。也就是说,“未来与过去无关”,当前的状态是此前历史的一个完整总结,此前的历史只能通过当前的状态去影响过程未来的演变。

无后效性:从上述定义可以看出 f [ k ] 的状态完全是从 f [ k 1 ] 转移过来,所以我们只要把k放到最外层循环中,就可以保证无后效性。

滚动数组(证明无后效性)

可以发现,整个程序一直在对同一个二维数组进行操作,而不是因为最外层循环有k层,就建立k个二维数组。这是因为这个二维数组可以在更新的过程一直重复利用,而且按照程序流程走,不会出现错误的情况。
假设初始二维数组 f [ 0 ] 为:
这里写图片描述
最外层循环k从1开始, f [ 1 ] [ i ] [ j ] 只和 f [ 0 ] 有关。
第一种情况 f [ 1 ] [ i ] [ j ] = f [ 0 ] [ i ] [ j ]
第二种情况 f [ 1 ] [ i ] [ j ] = f [ 0 ] [ i ] [ 1 ] + f [ 0 ] [ 1 ] [ j ] 这两种情况

参与计算的值:指等号右边的值

下面分析在k=1时的最外层循环中:
第一种情况中,参与计算的值就是它本身(因为使用的是同一个二维数组,而 i j 又是一样的)(符合无后效性: f [ 1 ] 的状态完全是从 f [ 0 ] 转移过来)
第二种情况中,参与计算的值由两部分组成 f [ 0 ] [ i ] [ 1 ] + f [ 0 ] [ 1 ] [ j ]
f [ 0 ] [ i ] [ 1 ] 是第1列元素(i从1变化到4,所以是第1列)且 f [ 0 ] [ 1 ] [ j ] 是第1行元素(j从1变化到4,所以是第1行)。
蓝线代表对角线元素。
这里写图片描述
既然k=1确定下来了,再确定i和j,i和j无非就是下面的情况:
( 1 , 1 ) ( 1 , 2 ) ( 1 , 3 ) ( 1 , 4 ) ( 2 , 1 ) ( 2 , 2 ) . . . ( 4 , 4 )
括号内的取值代表 ( i , j ) 。且循环是按照上面的情况,依次顺序执行

那么k=1层循环中的某个时刻,这个二维数组的元素有的是属于 f [ 1 ] 的,有的是属于 f [ 0 ] 。比如,有可能是 ( 1 , 1 ) ( 1 , 2 ) ( 1 , 3 ) ( 1 , 4 ) 元素是属于 f [ 1 ] 的, ( 2 , 1 ) ( 2 , 2 ) . . . ( 4 , 4 ) 是属于 f [ 0 ] 。因为循环还没有执行完毕嘛。

现在需要做的证明是,因为有 f [ 1 ] 的状态完全是从 f [ 0 ] 转移过来,所以当你计算任意的 f [ 1 ] [ i ] [ j ] 的过程中,参与计算的值应该是都属于 f [ 0 ] 的,而不是属于 f [ 1 ] 的(即证明无后效性)

为了证明,现作证明的准备,分析上面提到的第二种情况中参与计算的值的组成的两部分 f [ 0 ] [ i ] [ 1 ] + f [ 0 ] [ 1 ] [ j ] ,假设 ( i , j ) 现在确定下来为 ( 2 , 3 ) (且这里假设符第二种情况),那么有 f [ 1 ] [ 2 ] [ 3 ] = f [ 0 ] [ 2 ] [ 1 ] + f [ 0 ] [ 1 ] [ 3 ]

但由于程序是顺序执行的, ( 2 , 1 ) ( 1 , 3 ) 肯定是在 ( 2 , 3 ) 之前就执行了,意思就是现在我们已经找不到 f [ 0 ] [ 2 ] [ 1 ] f [ 0 ] [ 1 ] [ 3 ] 的值,因为已经被 f [ 1 ] [ 2 ] [ 1 ] f [ 1 ] [ 1 ] [ 3 ] 覆盖掉了啊,既然如此,再按照 f [ 1 ] [ 2 ] [ 3 ] = f [ 0 ] [ 2 ] [ 1 ] + f [ 0 ] [ 1 ] [ 3 ] 计算,实际是计算的是 f [ 1 ] [ 2 ] [ 3 ] = f [ 1 ] [ 2 ] [ 1 ] + f [ 1 ] [ 1 ] [ 3 ] ,那岂不是违背了我们要做的证明(即无后效性)。

但其实并没有违背,因为 f [ 0 ] [ 2 ] [ 1 ] f [ 1 ] [ 2 ] [ 1 ] 是一样的,同样, f [ 0 ] [ 1 ] [ 3 ] f [ 1 ] [ 1 ] [ 3 ] 也是一样的。因为 f [ 1 ] [ 2 ] [ 1 ] = f [ 0 ] [ 2 ] [ 1 ] + f [ 0 ] [ 1 ] [ 1 ] ,但 f [ 0 ] [ 1 ] [ 1 ] = 0 (图中对角线的元素都为0),所以就有,实际计算的 f [ 1 ] [ 2 ] [ 3 ] = f [ 1 ] [ 2 ] [ 1 ] + f [ 1 ] [ 1 ] [ 3 ] 并不违背无后效性。

进一步分析推广,得出,在红线上的元素对于 f [ 1 ] [ i ] [ j ] = f [ 0 ] [ i ] [ j ] 永远成立

证明准备完毕,继续证明。情况无非两种,第一种情况和第二种情况
第一种情况 f [ 1 ] [ i ] [ j ] = f [ 0 ] [ i ] [ j ] ,等号左边的元素的都是k=1,等号右边的元素都是k=0,符合证明。
第二种情况 f [ 1 ] [ i ] [ j ] = f [ 0 ] [ i ] [ 1 ] + f [ 0 ] [ 1 ] [ j ] .参与计算的值的左边元素和右边元素,都是属于红线上的元素,根据上述证明的准备,得知参与计算的值的元素不管有没有被覆盖掉(从 f [ 0 ] 变到 f [ 1 ] ),其值与 f [ 0 ] 相等,即不变。即等号右边的元素都是k=0,符合证明。

所以,只用一个二维数组,即滚动数组,是可以的。因为就算反复只用一个数组,计算值的过程中,也一直保持了正确性,即保持了无后效性。
同理分析k=2,k=3,k=4时的最外层循环,也同样能证明在程序运行的过程中,保持了无后效性。

总结

负权边:权重为负数的边。
负权环:源点到源点的一个环,环上权重和为负数。

Floyd算法只能在不存在负权环的情况下使用。如果有负权环,那么最短路径将无意义,因为我们可以不断走负权环,这样最短路径值便成为了负无穷。但可以处理带负权边但是无负权环的情况。

猜你喜欢

转载自blog.csdn.net/anlian523/article/details/80925625