leetcode-master/problems/kamacoder/0097.小明逛公园.md

12 KiB
Raw Blame History

Floyd 算法精讲

卡码网97. 小明逛公园

【题目描述】

小明喜欢去公园散步,公园内布置了许多的景点,相互之间通过小路连接,小明希望在观看景点的同时,能够节省体力,走最短的路径。

给定一个公园景点图,图中有 N 个景点(编号为 1 到 N以及 M 条双向道路连接着这些景点。每条道路上行走的距离都是已知的。

小明有 Q 个观景计划,每个计划都有一个起点 start 和一个终点 end表示他想从景点 start 前往景点 end。由于小明希望节省体力他想知道每个观景计划中从起点到终点的最短路径长度。 请你帮助小明计算出每个观景计划的最短路径长度。

【输入描述】

第一行包含两个整数 N, M, 分别表示景点的数量和道路的数量。

接下来的 M 行,每行包含三个整数 u, v, w表示景点 u 和景点 v 之间有一条长度为 w 的双向道路。

接下里的一行包含一个整数 Q表示观景计划的数量。

接下来的 Q 行,每行包含两个整数 start, end表示一个观景计划的起点和终点。

【输出描述】

对于每个观景计划,输出一行表示从起点到终点的最短路径长度。如果两个景点之间不存在路径,则输出 -1。

【输入示例】

7 3 1 2 4 2 5 6 3 6 8 2 1 2 2 3

【输出示例】

4 -1

【提示信息】

从 1 到 2 的路径长度为 42 到 3 之间并没有道路。

1 <= N, M, Q <= 1000.

思路

本题是经典的多源最短路问题。

在这之前我们讲解过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的最短距离的距离组成。

那么选哪个呢?

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

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

之前在讲解动态规划的时候,给出过动规五部曲:

  • 确定dp数组dp table以及下标的含义
  • 确定递推公式
  • dp数组如何初始化
  • 确定遍历顺序
  • 举例推导dp数组

那么接下来我们还是用这五部来给大家讲解 Floyd。

1、确定dp数组dp table以及下标的含义

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

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

可能有录友会想: 节点i 到 节点j 的最短距离为m这句话可以理解但 以[1...k]集合为中间节点 理解不辽。

节点i 到 节点j 的最短路径中 一定是经过很多节点,那么这个集合用[1...k] 来表示。

k不能单独指某个节点因为谁说 节点i 到节点j的最短路径中 一定只有一个节点呢所以k 一定要表示一个集合,即[1...k] 表示节点1 到 节点k 一共k个节点的集合。

2、确定递推公式

在上面的分析中我们已经初步感受到了递推的关系。

我们分两种情况:

  1. 节点i 到 节点j 的最短路径经过节点k
  2. 节点i 到 节点j 的最短路径不经过节点k

对于第一种情况,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]

第二种情况,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、dp数组如何初始化

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

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

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

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

所以 只能 把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; // 注意这里是双向图
} 

grid数组中其他元素数值应该初始化多少呢

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

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

所以grid数组的定义可以是

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

4、确定遍历顺序

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

而 k 依赖于 k - 1 i 和j 的到 并不依赖与 i - 1 或者 j - 1 等等。

那么这三个for的嵌套顺序应该是什么样的呢

我们来看初始化,我们是把 k =0 的 i 和j 对应的数值都初始化了,这样才能去计算 k = 1 的时候 i 和 j 对应的数值。

这就好比是一个三维坐标i 和j 是平层而k 是 垂直向上 的。

遍历的顺序是从底向上 一层一层去遍历。

所以遍历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]);
        }
    }
}

有录友可能想,难道 遍历k 放在最里层就不行吗?

k 放在最里层,代码是这样:

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

此时就遍历了 j 与 k 形成一个平面i 则是纵面,那遍历 就是这样的:

而我们初始化,是 k 为0然后 i 和 j 形成的平面做初始化,如果以 k 和 j 形成的平面去遍历,就造成了 递推公式 用不上上一轮计算的结果,从而导致结果不对(初始化的结果只能用上一部分,因为初始化是 i 与j 形成的平面)。

我再给大家举一个测试用例

5 4
1 2 10
1 3 1
3 4 1
4 2 1
1
1 2

就是图:

就节点1 到 节点 2 的最短距离,运行结果是 10 但正确的结果很明显是3。

为什么呢?

因为 k 放在最里面,先就把 节点1 和 节点 2 的最短距离就确定了,后面再也不会计算节点 1 和 节点 2的距离同时也不会基于 初始化或者之前计算过的结果来计算,即不会考虑 节点1 到 节点3 节点3 到节点 4节点4到节点2 的距离。

而遍历k 的for循环如果放在中间呢同样是 j 与k 行程一个平面i 是纵面,遍历的也是这样:

同样不能完全用上初始化 和 上一层计算的结果。

很多录友对于 floyd算法的遍历顺序搞不懂其实 是没有从三维的角度去思考,同时我把三维立体图给大家画出来,遍历顺序标出来,大家就很容易想明白,为什么 k 放在最外层 才能用上 初始化和上一轮计算的结果了。

#include <iostream>
#include <vector>
#include <list>
using namespace std;

int main() {
    int n, m, p1, p2, val;
    cin >> n >> m;

    vector<vector<vector<int>>> grid(n + 1, vector<vector<int>>(n + 1, vector<int>(n + 1, 10005)));  // 因为边的最大距离是10^4
    for(int i = 0; i < m; i++){
        cin >> p1 >> p2 >> val;
        grid[p1][p2][0] = val;
        grid[p2][p1][0] = val; // 注意这里是双向图

    }
    // 开始 floyd
    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]);
            }
        }
    }
    // 输出结果
    int z, start, end;
    cin >> z;
    while (z--) {
        cin >> start >> end;
        if (grid[start][end][n] == 10005) cout << -1 << endl;
        else cout << grid[start][end][n] << endl;
    }
}

拓展 负权回路

本题可以有负数,但不能出现负权回路


floyd n^3

同样多源汇最短路算法 Floyd 也是基于动态规划

Floyd 算法可以用来解决多源最短路径问题,它会计算图中每两个点之间的最短路径。

Floyd 算法对边权的正负没有限制要求(可处理正负权边的图),且能利用 Floyd 算法可能够对图中负环进行判定

LeetCode-1334. 阈值距离内邻居最少的城市

https://leetcode.cn/problems/find-the-city-with-the-smallest-number-of-neighbors-at-a-threshold-distance/description/


#include <iostream>
#include <vector>
#include <list>
using namespace std;

int main() {
    int n, m, p1, p2, val;
    cin >> n >> m;

    vector<vector<int>> grid(n, vector<int>(n, 10005));  // 因为边的最大距离是10^4

    for(int i = 0; i < m; i++){
        cin >> p1 >> p2 >> val;
        grid[p1][p2] = val;
        grid[p2][p1] = val; // 注意这里是双向图

    }
    // 开始 floyd
    for (int p = 0; p < n; p++) {
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                grid[i][j] = min(grid[i][j], grid[i][p] + grid[p][j]);
            }
        }
    }
    // 输出结果
    int z, start, end;
    cin >> z;
    while (z--) {
        cin >> start >> end; 
        if (grid[start][end] == 10005) cout << -1 << endl;
        else cout << grid[start][end] << endl;
    }
}