图论 24. Floyd算法(多源最短路问题)

图论 24. Floyd算法(多源最短路问题)

97. 小明逛公园

代码随想录

卡码网无难度标识

相对于前面的单源最短路解法,这道题扩展到了多源最短路问题。

代码随想录:

理解了遍历顺序才是floyd算法最精髓的地方。

floyd算法的时间复杂度相对较高,适合 稠密图且源点较多的情况。

如果是稀疏图,floyd是从节点的角度去计算了,例如 图中节点数量是 1000,就一条边,那 floyd的时间复杂度依然是 O(n^3) 。

如果 源点少,其实可以 多次dijsktra 求源点到终点。

  • 思路:(摘录修改自代码随想录)

    • 题目解析:

      dijkstra朴素版、dijkstra堆优化、Bellman算法、Bellman队列优化(SPFA) 都是单源最短路,即只能有一个起点。

      而本题是多源最短路,即 求多个起点到多个终点的多条最短路径。

      这就需要用到一个新的最短路算法:Floyd 算法

      Floyd 算法对边的权值正负没有要求,都可以处理

      Floyd算法核心思想是动态规划。

      • 例如我们再求节点1 到 节点9 的最短距离,用二维数组来表示即:grid[1][9],如果最短距离是10 ,那就是 grid[1][9] = 10。

        节点1 到 节点9 的最短距离 可以由 (节点1 到节点5的最短距离 + 节点5到节点9的最短距离)组成

        即 grid[1][9] = grid[1][5] + grid[5][9]。

        节点1 到节点5的最短距离 可以由 节点1 到 节点3的最短距离 + 节点3 到 节点5 的最短距离组成,

        即 grid[1][5] = grid[1][3] + grid[3][5]

        以此类推,节点1 到 节点3的最短距离 可以由更小的区间组成。

        那么这样我们就找到了,子问题推导求出整体最优方案的递归关系。

      • 若节点1 到 节点9 的最短距离 可以由 节点1 到节点5的最短距离 + 节点5到节点9的最短距离组成, 也可以有 节点1 到节点7的最短距离 + 节点7 到节点9的最短距离的距离组成。

        选哪个呢?

        显然要选一个最小的,毕竟是求最短路。

        此时我们已经接近明确递归公式了。

    • Floyd算法的动态规划五部曲:

      1. dp定义:

        这里用 grid数组来存图,那就把dp数组命名为 grid。

        grid[i][j][k] = m​,表示从顶点 i 到顶点 j 的最短路径长度,其中只允许中间结点集合为 {1, 2, …, k} 中的顶点。

        注意:

        • 路径中所有的中间顶点(不包括起点 i​ 和终点 j​)都必须来自集合 {1, 2, …, k}​。
        • 这并不意味着路径中只能有一个中间顶点,而是可以有多个,只要它们的编号都不超过 k​。
      2. 确定递推公式:

        分两种情况:

        1. 情况一:节点i 到 节点j 的最短路径经过节点k

          相当于将 i 到 j 的距离用结点 k 分成了 i 到 k 和 k 到 j 两部分。

          grid[i][j][k] = grid[i][k][k - 1] + grid[k][j][k - 1]

          节点i 到 节点k 的最短距离 必然不经过节点k,中间节点集合为[1...k-1],所以 表示为grid[i][k][k - 1]

          节点k 到 节点j 的最短距离 必然也不经过节点k自己,中间节点集合为[1...k-1],所以表示为 grid[k][j][k - 1]

        2. 情况二:节点i 到 节点j 的最短路径不经过节点k

          grid[i][j][k] = grid[i][j][k - 1]

          如果节点i 到 节点j的最短距离 不经过节点k,那么 中间节点集合[1...k-1],表示为 grid[i][j][k - 1]

        因为我们是求最短路,对于这两种情况自然是取最小值。

        即: grid[i][j][k] = min(grid[i][k][k - 1] + grid[k][j][k - 1], grid[i][j][k - 1])

      3. 初始化:

        grid[i][j][k] = m​,表示 节点i 到 节点j 以[1...k] 集合为中间节点的最短距离为m。

        grid数组中所有元素数值初始化默认值为无穷大:

        本题求的是最小值,所以输入数据没有涉及到的节点的情况都应该初始为一个最大数。

        这样才不会影响,每次计算去最小值的时候 初始值对计算结果的影响。

        所以grid数组的定义可以是:

        // C++写法,定义了一个三位数组,10005是因为边的最大距离是10^4
        vector<vector<vector<int>>> grid(n + 1, vector<vector<int>>(n + 1, vector<int>(n + 1, 10005)));  
        

        初始状态下(即 k = 0),路径只能直接走边,不经过任何中间顶点。

        所以对于边集合内的所有边(两端点为p1和p2,边权重为val),初始化为:

        grid[p1][p2][0] = val, grid[p2][p1][0] = val​,

        即p1到p2之间,不经过任何中间顶点,对应的grid(或者说dp)值,为无向边p1 - p2的长度/权重。

        代码随想录在这里的解释令人头大,但依然摘下来作为一个补充:

        刚开始初始化k 是不确定的。

        例如题目中只是输入边(节点2 -> 节点6,权值为3),那么grid[2][6][k] = 3,k需要填什么呢?

        把k 填成1,那如何上来就知道 节点2 经过节点1 到达节点6的最短距离是多少 呢。

        所以 只能 把k 赋值为 0,本题 节点0 是无意义的,节点是从1 到 n。

        这样我们在下一轮计算的时候,就可以根据 grid[i][j][0] 来计算 grid[i][j][1],此时的 grid[i][j][1] 就是 节点i 经过节点1 到达 节点j 的最小距离了。

        初始化代码:

        vector<vector<vector<int>>> grid(n + 1, vector<vector<int>>(n + 1, vector<int>(n + 1, 10005)));  // C++定义了一个三位数组,10005是因为边的最大距离是10^4
        
        for(int i = 0; i < m; i++){
            cin >> p1 >> p2 >> val;
            grid[p1][p2][0] = val;
            grid[p2][p1][0] = val; // 注意这里是双向图
        } 
        
        
      4. 遍历顺序:

        用三维视角去看待grid:将 i 和 j 视为xy平面,k 视为z轴。

        从递推公式:grid[i][j][k] = min(grid[i][k][k - 1] + grid[k][j][k - 1], grid[i][j][k - 1])​ 可以看出,我们需要三个for循环,分别遍历i,j 和k

        而 k 依赖于 k - 1, i 和j 的却 并不依赖于 i - 1 或者 j - 1。

        显然,初始化的时候是k = 0(z轴零点处)初始化了整个xy平面,

        之后的迭代要按z轴从底向上 一层一层去遍历。

        也就是遍历k 的for循环一定是在最外面,

        至于遍历 i 和 j 的话,for 循环的先后顺序则无所谓。

        代码如下:

        for (int k = 1; k <= n; k++) {
            for (int i = 1; i <= n; i++) {
                for (int j = 1; j <= n; j++) {
                    grid[i][j][k] = min(grid[i][j][k-1], grid[i][k][k-1] + grid[k][j][k-1]);
                }
            }
        }
        
  • 个人代码:

    import sys
    
    lines = sys.stdin.readlines()
    n, m = map(int, lines[0].strip().split()) # 景点/结点数n,道路/边数m
    q = int(lines[m+1].strip()) # 观景计划的数量(源点->终点pair的数量)
    
    # 创建dp数组grid,大小为(n+1)*(n+1)*(n+1),结点编号1~n
    # grid[i][j][k]表示节点i 到 节点j 以[1...k] 集合为中间节点的最短距离
    grid = [[[float('inf')] * (n + 1) for _ in range(n + 1)] for _ in range(n + 1)]
    for i in range(1, m + 1):
        # 双向边 u - v,边权值val
        u, v, val = map(int, lines[i].strip().split())
        grid[u][v][0] = val
        grid[v][u][0] = val # 双向边,初始化grid[i][j][0] = val
    
    # 遍历递推,结点编号1~n(floyd关键代码)
    for k in range(1, n + 1):
        for i in range(1, n + 1): # i和j取0没有意义,直接不管就行
            for j in range(1, n + 1):
                grid[i][j][k] = min(
                    grid[i][j][k-1], # 要么i到j不经过结点k,则最优方案与前一层(第三维,k-1层)一致;
                    grid[i][k][k-1] + grid[k][j][k-1] # 要么i到j经过k结点,则当前最优方案等于前一层(第三维,k-1层)的i到k最优方案加上k到j最优方案                
                )
    
    # 根据(源点,终点)对,打印对应的最短路径长度
    for i in range(m + 2, len(lines)):
        # 观景计划的源点和终点
        start, end = map(int, lines[i].strip().split())
        if grid[start][end][n] == float('inf'): # 不存在路径
            print(-1)
        else:
            print(grid[start][end][n])
    
  • Floyd空间优化:(摘录修改自代码随想录)

    • 滚动数组的角度来看,我们定义一个 grid[n + 1][ n + 1][2]这么大的数组就可以

      因为k 只是依赖于 k-1的状态,并不需要记录k-2,k-3,k-4 等等这些状态。

      那么我们只需要记录 grid[i][j][1] 和 grid[i][j][0] 就好,之后就是 grid[i][j][1] 和 grid[i][j][0] 交替滚动。

      (或者笔者个人其实更喜欢直接开一个oldGrid数组存储前一层的grid,更加直观一些)

    • 再进一步想,

      如果本层计算(本层计算即k相同,从三维角度来讲) gird[i][j] 用到了 本层中刚计算好的 grid[i][k] 会有什么问题吗?

      如果 本层刚计算好的 grid[i][k] 比上一层 (即k-1层)计算的 grid[i][k] 小,说明确实有 i 到 k 的更短路径,那么基于 更小的 grid[i][k] 去计算 gird[i][j] 没有问题。

      如果 本层刚计算好的 grid[i][k] 比上一层 (即k-1层)计算的 grid[i][k] 大, 这不可能,因为这样也不会做更新 grid[i][k]的操作。

      所以本层计算中,使用了本层计算过的 grid[i][k] 和 grid[k][j] 是没问题的。

      那么就没必要区分,grid[i][k] 和 grid[k][j] 是 属于 k - 1 层的呢,还是 k 层的。

      (实际上和bellman-ford算法中原地更新的写法不会出错的原因,是基于同样的考虑!)

    所以递归公式可以为:

    grid[i][j] = min(grid[i][j], grid[i][k] + grid[k][j]);
    

    空间优化后代码:

    • 时间复杂度: O(n^3)
    • 空间复杂度:O(n^2)
    import sys
    
    lines = sys.stdin.readlines()
    n, m = map(int, lines[0].strip().split()) # 景点/结点数n,道路/边数m
    q = int(lines[m+1].strip()) # 观景计划的数量(源点->终点pair的数量)
    
    # 创建dp数组grid,大小为(n+1)*(n+1),结点编号1~n
    # grid[i][j]表示节点i 到 节点j 以当前层集合为中间节点的最短距离
    # (设当前层为k,则[1...k]为当前层可以取用中间节点的集合)
    grid = [[float('inf')] * (n + 1) for _ in range(n + 1)]
    for i in range(1, m + 1):
        # 双向边 u - v,边权值val
        u, v, val = map(int, lines[i].strip().split())
        grid[u][v] = val
        grid[v][u] = val # 双向边,初始化层k=0时,grid[i][j] = val
    
    # 遍历递推,结点编号1~n
    for k in range(1, n + 1): # 层k从1到n递推
        for i in range(1, n + 1): # i和j取0没有意义,直接不管就行
            for j in range(1, n + 1):
                grid[i][j] = min(
                    grid[i][j], # 要么i到j不经过当前结点k,则最优方案与前一层(第三维,k-1层)一致;
                    grid[i][k] + grid[k][j] # 要么i到j经过当前结点k,则当前最优方案等于前一层(第三维,k-1层)的i到k最优方案加上k到j最优方案                
                )
    
    # 根据(源点,终点)对,打印对应的最短路径长度
    for i in range(m + 2, len(lines)):
        # 观景计划的源点和终点
        start, end = map(int, lines[i].strip().split())
        if grid[start][end] == float('inf'): # 不存在路径
            print(-1)
        else:
            print(grid[start][end])
    

你可能感兴趣的:(小白的代码随想录刷题笔记,Mophead的小白刷题笔记,leetcode,python,代码随想录,图论)