Files
leetcode-master/problems/kamacoder/0097.小明逛公园.md
youngyangyang04 a6c33f859f Update
2025-09-30 15:22:50 +08:00

581 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<p align="center"><strong><a href="./qita/join.md">参与本项目</a>,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!</strong></p>
# Floyd 算法精讲
[卡码网97. 小明逛公园](https://kamacoder.com/problempage.php?pid=1155)
【题目描述】
小明喜欢去公园散步,公园内布置了许多的景点,相互之间通过小路连接,小明希望在观看景点的同时,能够节省体力,走最短的路径。
给定一个公园景点图,图中有 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] 来表示。
你可以反过来想节点i 到 节点j 中间一定经过很多节点,那么你能用什么方式来表述中间这么多节点呢?
所以 这里的k不能单独指某个节点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的最短距离是多少 呢。
所以 只能 把k 赋值为 0本题 节点0 是无意义的节点是从1 到 n。
这样我们在下一轮计算的时候,就可以根据 grid[i][j][0] 来计算 grid[i][j][1],此时的 grid[i][j][1] 就是 节点i 经过节点1 到达 节点j 的最小距离了。
grid数组是一个三维数组那么我们初始化的数据在 i 与 j 构成的平层,如图:
![](https://file1.kamacoder.com/i/algo/20240425104247.png)
红色的 底部一层是我们初始化好的数据,注意:从三维角度去看初始化的数据很重要,下面我们在聊遍历顺序的时候还会再讲。
所以初始化代码:
```CPP
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数组的定义可以是
```CPP
// 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循环一定是在最外面这样才能一层一层去遍历。如图
![](https://file1.kamacoder.com/i/algo/20240424120109.png)
至于遍历 i 和 j 的话for 循环的先后顺序无所谓。
代码如下:
```CPP
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 放在最里层,代码是这样:
```CPP
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 则是纵面,那遍历 就是这样的:
![](https://file1.kamacoder.com/i/algo/20240424115827.png)
而我们初始化的数据 是 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
```
就是图:
![](https://file1.kamacoder.com/i/algo/20240424120942.png)
求节点1 到 节点 2 的最短距离,运行结果是 10 但正确的结果很明显是3。
为什么呢?
因为 k 放在最里面,先就把 节点1 和 节点 2 的最短距离就确定了,后面再也不会计算节点 1 和 节点 2的距离同时也不会基于 初始化或者之前计算过的结果来计算,即:不会考虑 节点1 到 节点3 节点3 到节点 4节点4到节点2 的距离。
造成这一原因,是 在三维立体坐标中, 我们初始化的是 i 和 i 在k 为0 所构成的平面,但遍历的时候 是以 j 和 k构成的平面以 i 为垂直方向去层次遍历。
而遍历k 的for循环如果放在中间呢同样是 j 与k 行程一个平面i 是纵面,遍历的也是这样:
![](https://file1.kamacoder.com/i/algo/20240424115827.png)
同样不能完全用上初始化 和 上一层计算的结果。
根据这个情况再举一个例子:
```
5 2
1 2 1
2 3 10
1
1 3
```
图:
![](https://file1.kamacoder.com/i/algo/20240425112636.png)
求 节点1 到节点3 的最短距离如果k循环放中间程序的运行结果是 -1也就是不能到达节点3。
在计算 grid[i][j][k] 的时候,需要基于 grid[i][k][k-1] 和 grid[k][j][k-1]的数值。
也就是 计算 grid[1][3][2] 表示节点1 到 节点3经过节点2 的时候,需要基于 grid[1][2][1] 和 grid[2][3][1]的数值,而 我们初始化,只初始化了 k为0 的那一层。
造成这一原因 依然是 在三维立体坐标中, 我们初始化的是 i 和 j 在k 为0 所构成的平面,但遍历的时候 是以 j 和 k构成的平面以 i 为垂直方向去层次遍历。
很多录友对于 floyd算法的遍历顺序搞不懂**其实 是没有从三维的角度去思考**,同时我把三维立体图给大家画出来,遍历顺序标出来,大家就很容易想明白,为什么 k 放在最外层 才能用上 初始化和上一轮计算的结果了。
5、举例推导dp数组
这里涉及到 三维矩阵可以一层一层打印出来去分析例如k=0 的这一层k = 1的这一层但一起把三维带数据的图画出来其实不太好画。
## 代码如下
以上分析完毕,最后代码如下:
```CPP
#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;
}
}
```
## 空间优化
这里 我们可以做一下 空间上的优化,从滚动数组的角度来看,我们定义一个 grid[n + 1][ n + 1][2] 这么大的数组就可以因为k 只是依赖于 k-1的状态并不需要记录k-2k-3k-4 等等这些状态。
那么我们只需要记录 grid[i][j][1] 和 grid[i][j][0] 就好,之后就是 grid[i][j][1] 和 grid[i][j][0] 交替滚动。
在进一步想如果本层计算本层计算即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 层的。
所以递归公式可以为:
```CPP
grid[i][j] = min(grid[i][j], grid[i][k] + grid[k][j]);
```
基于二维数组的本题代码为:
```CPP
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n, m, p1, p2, val;
cin >> n >> m;
vector<vector<int>> grid(n + 1, vector<int>(n + 1, 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 k = 1; k <= n; k++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
grid[i][j] = min(grid[i][j], grid[i][k] + grid[k][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;
}
}
```
* 时间复杂度: O(n^3)
* 空间复杂度O(n^2)
## 总结
本期如果上来只用二维数组来讲的话,其实更容易,但遍历顺序那里用二维数组其实是讲不清楚的,所以我直接用三维数组来讲,目的是将遍历顺序这里讲清楚。
理解了遍历顺序才是floyd算法最精髓的地方。
floyd算法的时间复杂度相对较高适合 稠密图且源点较多的情况。
如果是稀疏图floyd是从节点的角度去计算了例如 图中节点数量是 1000就一条边那 floyd的时间复杂度依然是 O(n^3) 。
如果 源点少,其实可以 多次dijsktra 求源点到终点。
## 其他语言版本
### Java
- 基于三维数组的Floyd算法
```java
public class FloydBase {
// public static int MAX_VAL = Integer.MAX_VALUE;
public static int MAX_VAL = 10005; // 边的最大距离是10^4(不选用Integer.MAX_VALUE是为了避免相加导致数值溢出)
public static void main(String[] args) {
// 输入控制
Scanner sc = new Scanner(System.in);
System.out.println("1.输入N M");
int n = sc.nextInt();
int m = sc.nextInt();
System.out.println("2.输入M条边");
// ① dp定义grid[i][j][k] 节点i到节点j 可能经过节点Kk∈[1,n]))的最短路径
int[][][] grid = new int[n + 1][n + 1][n + 1];
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
for (int k = 0; k <= n; k++) {
grid[i][j][k] = grid[j][i][k] = MAX_VAL; // 其余设置为最大值
}
}
}
// ② dp 推导grid[i][j][k] = min{grid[i][k][k-1] + grid[k][j][k-1], grid[i][j][k-1]}
while (m-- > 0) {
int u = sc.nextInt();
int v = sc.nextInt();
int weight = sc.nextInt();
grid[u][v][0] = grid[v][u][0] = weight; // 初始化处理k=0的情况 ③ dp初始化
}
// ④ dp推导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] = Math.min(grid[i][k][k - 1] + grid[k][j][k - 1], grid[i][j][k - 1]);
}
}
}
System.out.println("3.输入[起点-终点]计划个数");
int x = sc.nextInt();
System.out.println("4.输入每个起点src 终点dst");
while (x-- > 0) {
int src = sc.nextInt();
int dst = sc.nextInt();
// 根据floyd推导结果输出计划路径的最小距离
if (grid[src][dst][n] == MAX_VAL) {
System.out.println("-1");
} else {
System.out.println(grid[src][dst][n]);
}
}
}
}
```
### Python
基于三维数组的Floyd
```python
if __name__ == '__main__':
max_int = 10005 # 设置最大路径因为边最大距离为10^4
n, m = map(int, input().split())
grid = [[[max_int] * (n+1) for _ in range(n+1)] for _ in range(n+1)] # 初始化三维dp数组
for _ in range(m):
p1, p2, w = map(int, input().split())
grid[p1][p2][0] = w
grid[p2][p1][0] = w
# 开始floyd
for k in range(1, n+1):
for i in range(1, n+1):
for j in range(1, n+1):
grid[i][j][k] = min(grid[i][j][k-1], grid[i][k][k-1] + grid[k][j][k-1])
# 输出结果
z = int(input())
for _ in range(z):
start, end = map(int, input().split())
if grid[start][end][n] == max_int:
print(-1)
else:
print(grid[start][end][n])
```
基于二维数组的Floyd
```python
if __name__ == '__main__':
max_int = 10005 # 设置最大路径因为边最大距离为10^4
n, m = map(int, input().split())
grid = [[max_int]*(n+1) for _ in range(n+1)] # 初始化二维dp数组
for _ in range(m):
p1, p2, val = map(int, input().split())
grid[p1][p2] = val
grid[p2][p1] = val
# 开始floyd
for k in range(1, n+1):
for i in range(1, n+1):
for j in range(1, n+1):
grid[i][j] = min(grid[i][j], grid[i][k] + grid[k][j])
# 输出结果
z = int(input())
for _ in range(z):
start, end = map(int, input().split())
if grid[start][end] == max_int:
print(-1)
else:
print(grid[start][end])
```
### Go
### Rust
### JavaScript
### TypeScript
### PhP
### Swift
### Scala
### C#
### Dart
### C