Floyd弗洛伊德任意最短单元路径杂谈——谈去维度

这几天,偶然翻开自己在B站上面的这篇文章,这篇文章总体讲的是没什么问题的,但是我觉得还是不太清楚,所以准备在这里进行重新写一篇博客作为补充。

原文回顾

原文地址:
https://b23.tv/1FQvrak

推荐阅读:(大佬的博客)
https://www.cnblogs.com/wangyuliang/p/9216365.html

核心思想快速回顾

其实本算法的核心思想真的很简单,那么就是考虑图中每一个点,如果被当作中转点,那么对其他两个顶点之间的路径是否可以产生贡献。

就比如我们有N个顶点。我们思考第N个顶点是否对C(N,2)个顶点之间的路径可以优化。那么我们设计的思路是从第1个节点开始进行计算优化,然后第2个节点能否产生优化是建立在从第1个节点能否优化之上的。这点跟动态规划有一样的思想。

也就是第N个节点的决策,建立在第N-1个节点之前的决策之上。
现在请看推导公式:
公式推导
第k层的意思就是想要告诉你,我们正在思考第N个节点。
那么k和k-1的关系就可以轻松推导。
请添加图片描述
所以我们现在可以写出这样的代码:

    for (int k = 1; k <= V; k++) {
        for (int i = 1; i <= V; i++) {
            for (int j = 1; j <= V; j++) {
                dp[k][i][j] = min(dp[k - 1][i][j], dp[k - 1][i][k] + dp[k - 1][k][j]);
            }
        }
    }

但是你以为我写到这里就结束了嘛,错!我想说的现在才刚刚开始。

减少空间复杂度

我们在网上一般看到的版本应该是形如下面的版本:
d p [ i ] [ j ] = m i n ( d p [ i ] [ k ] + d p [ k ] [ j ] , d p [ i ] [ j ] ) dp[i][j]=min(dp[i][k]+dp[k][j],dp[i][j]) dp[i][j]=min(dp[i][k]+dp[k][j],dp[i][j])
其实这相当于什么,这相当于:
公式左边的 d p [ i ] [ j ] dp[i][j] dp[i][j]就相当于 d p [ k ] [ i ] [ j ] dp[k][i][j] dp[k][i][j]
d p [ i ] [ k ] dp[i][k] dp[i][k]就相当于 d p [ k − 1 ] [ i ] [ k ] dp[k-1][i][k] dp[k1][i][k]
同理的 d p [ k ] [ j ] dp[k][j] dp[k][j]就相当于 d p [ k − 1 ] [ k ] [ j ] dp[k-1][k][j] dp[k1][k][j]

好,我们首先把k当初z轴,从下往上的那个轴心,我们会发现,其实在内存中,我们用到的数据结构差不多只用到k轴和k-1轴之前的数据。

那么我们可以发现,k-1轴数据之前的数据其实是完全浪费的。
请看下面这张图片:
请添加图片描述
我们可以看出图中蓝色部分确实是没有必要进行保存的,因为我们不可能再次的去使用它了

那么你可能会说,使用滚动数组来上下翻滚上层和下层的数据,这个确实没啥错误,我是赞同的。

但是我想说的是:
我们为什么能去掉k的原因是,我们可以保证数据没有被污染。

被污染是什么意思?我们这么想:

核心解析

如果我们把k-1这部分都算完了,现在我们计算k这部分。只要 d p [ i ] [ j ] dp[i][j] dp[i][j]没有在第k次跟新,那么实际上 d p [ i ] [ j ] dp[i][j] dp[i][j]就还是第k-1的形状,(也就是在第k这部分执行了 d p [ i ] [ j ] = m i n ( d p [ i ] [ k ] + d p [ k ] [ j ] , d p [ i ] [ j ] ) dp[i][j]=min(dp[i][k]+dp[k][j],dp[i][j]) dp[i][j]=min(dp[i][k]+dp[k][j],dp[i][j])的话,那么 d p [ i ] [ j ] dp[i][j] dp[i][j]就是第k层的形状了)

下面我们来模拟一下这个过程:
现在您看到的是,做完 k = 4 k=4 k=4时候的dp表格
如下:请添加图片描述
然后现在我们看k=5这个时候,按照上面的代码,现在我们执行。
dp[1][1]
然而dp[1][1]的计算依赖于dp[1][5],和dp[5][1]。

如果dp[1][5]和dp[5][1]是k的形状,那么代码就出错了。(别忘记了,我们这里希望的是dp[1][5]相当于dp[4][1][5])
但是如果dp[1][5]和dp[5][1]是k-1的形状,那么代码就是正确的。

仔细想想,我们在k这层(k=5)这层才做过dp[1][1]根本没有在这k层做过dp[1][5]和dp[5][1]。所以,dp[1][5]和dp[5][1]还是k-1(k=4)的形状。

下图演示了k=5的时候进行dp[1][1]的样子:
请添加图片描述

然后下面进行运算:
dp[1][2]:请添加图片描述
过程也是和dp[1][1]差不多的,图中所示的蓝色线条的那两个数据,同样也是k-1的形状。

好,我们来看一个重头戏,思考dp[1][5]
d p [ 1 ] [ 5 ] = m i n ( d p [ 1 ] [ 5 ] , d p [ 1 ] [ 5 ] + d p [ 5 ] [ 5 ] ) dp[1][5]=min(dp[1][5],dp[1][5]+dp[5][5]) dp[1][5]=min(dp[1][5],dp[1][5]+dp[5][5])
喂喂,注意看仔细了, d p [ 1 ] [ 5 ] 和 d p [ 5 ] [ 5 ] dp[1][5]和dp[5][5] dp[1][5]dp[5][5]是第一次被读取,所以还是没有什么问题。还是k-1的形状。

但是请注意看这里:
我多加一列j=6捏
d p [ 1 ] [ 6 ] = m i n ( d p [ 1 ] [ 5 ] , d p [ 5 ] [ 6 ] ) dp[1][6]=min(dp[1][5],dp[5][6]) dp[1][6]=min(dp[1][5],dp[5][6])
等等,😱
这里的dp[1][5]不是k的形状来嘛,因为我们就在上一段计算过了,哇,那这样说岂不是这里的 d p [ 1 ] [ 5 ] 是 d p [ k ] [ 1 ] [ 5 ] 而 不 是 d p [ k − 1 ] [ 1 ] [ 5 ] 了 嘛 Q A Q dp[1][5]是dp[k][1][5]而不是dp[k-1][1][5]了嘛QAQ dp[1][5]dp[k][1][5]dp[k1][1][5]QAQ

但是其实你仔细看,我们在计算 d p [ 1 ] [ 5 ] dp[1][5] dp[1][5]的时候,用到了一个叫 d p [ 5 ] [ 5 ] dp[5][5] dp[5][5]的东西,而这个东西永远等于0,这就是奥妙所在,所以我们计算dp[1][5]的话相当于: d p [ 1 ] [ 5 ] = m i n ( d p [ 1 ] [ 5 ] , d p [ 1 ] [ 5 ] + 0 ) dp[1][5]=min(dp[1][5],dp[1][5]+0) dp[1][5]=min(dp[1][5],dp[1][5]+0) d p [ 5 ] [ 5 ] = 0 dp[5][5]=0 dp[5][5]=0)所以 d p [ 1 ] [ 5 ] dp[1][5] dp[1][5]再k层怎么计算还是k-1层的形状。所以在这个时候一直没有变过。

所以在j=6
d p [ 1 ] [ 6 ] = m i n ( d p [ 1 ] [ 5 ] , d p [ 5 ] [ 6 ] ) dp[1][6]=min(dp[1][5],dp[5][6]) dp[1][6]=min(dp[1][5],dp[5][6])这里的dp[1][5]你可以说是k的形状,也可以说是k-1的形状。

综上,确实[k]是可以被去除的,因为数据保证了不会被污染。在没有被k刷新前,dp数组保存的还是k-1层的数据。

再谈空间优化

那么我们在初学动态规划的时候我们一定还学过一个叫做背包的东西,我先带您简单回顾一下,原文的背包动态规划就是,考虑n个物品,在现有的背包空间下,组合出背包内价值最大的一种方案。

那么和上面的动态规划思考还是一样的,因为咱们还是在运筹学范围内呢。

第n个物品选不选,取决于前n-1个物品的决策。然后再使用一个维度来记录重量,然后我们得到了一个这样的公式:

定义 d p [ i ] [ j ] dp[i][j] dp[i][j]的i的意思是从第一个到第i个物体能否陪凑出重量为j的可能性,如果可能那么就为1,不可能那么就为0。

请添加图片描述

然而,同理的,我们使用公式只跟i-1有关,那么之前的i-2 i-3是不用存储的,自然而然我们就想到了,我们不需要存储这些东西。

那么我们是不是可以简写这个东西捏,因为i我们不需要保存那么多份,如下图:
请添加图片描述
蓝色这部分是不需要存储的,所以我们自然就想到了,去除i这个维度,变为:
d p [ j ] dp[j] dp[j]
那么如果j是从0到w的话,我们其中最重要的公式是:
d p [ j ] = ( d p [ j ] ∣ d p [ j − w [ i ] ] ) dp[j]=(dp[j]|dp[j-w[i]]) dp[j]=(dp[j]dp[jw[i]])
这里给出之前没有优化的方程:
d [ i ] [ j ] = ( d [ i − 1 ] [ j ] ∣ d p [ i − 1 ] [ j − w [ i ] ] ) d[i][j]=(d[i-1][j]|dp[i-1][j-w[i]]) d[i][j]=(d[i1][j]dp[i1][jw[i]])
其中 w [ i ] w[i] w[i]的意思是第i件物品的重量。

好,现在我们看,如果优化后的方程它的j顺序是j从0开始到最后结束
那么方程的 d p [ j ] = ( d p [ j ] ∣ d p [ j − w [ i ] ] ) dp[j]=(dp[j]|dp[j-w[i]]) dp[j]=(dp[j]dp[jw[i]])的后面的dp[j-w[i]]访问的可能就是第i层更改过的数据了,而我们总是期望我们所访问的数据应该是第i-1层的数据,(因为我们是从dp[0] dp[1] 这样,所以我们访问dp[1]这里面的数据已经是第i层次的形状了,而我们希望它的形状应该是第i-1的)

然而,如果j的顺序是倒过来的,意思就是如下:
请添加图片描述
这样就保证了修改后的公式
修改后 d p [ j ] = ( d p [ j ] ∣ d p [ j − w [ i ] ] ) dp[j]=(dp[j]|dp[j-w[i]]) dp[j]=(dp[j]dp[jw[i]])
相当于修改前的 d [ i ] [ j ] = ( d [ i − 1 ] [ j ] ∣ d p [ i − 1 ] [ j − w [ i ] ] ) d[i][j]=(d[i-1][j]|dp[i-1][j-w[i]]) d[i][j]=(d[i1][j]dp[i1][jw[i]])


好啦,此篇悼词完结~撒花~

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值