This commit is contained in:
programmercarl
2024-04-24 16:08:28 +08:00
parent a6ba82a380
commit 956e8530f1
13 changed files with 768 additions and 113 deletions

View File

@ -219,7 +219,7 @@ class Solution {
### Python
先遍历物品, 再遍历背包
先遍历背包, 再遍历物品
```python
class Solution:
def numSquares(self, n: int) -> int:
@ -234,7 +234,7 @@ class Solution:
return dp[n]
```
先遍历背包, 再遍历物品
先遍历物品, 再遍历背包
```python
class Solution:
def numSquares(self, n: int) -> int:
@ -389,7 +389,7 @@ function numSquares(n: number): number {
};
```
## C
### C
```c
#define min(a, b) ((a) > (b) ? (b) : (a))

View File

@ -1,57 +0,0 @@
# 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

View File

@ -19,7 +19,7 @@
【输出描述】
输出一个整数,代表小明在途中和其他科学家和科研团队交流所花费的最时间。
输出一个整数,代表小明从起点到终点所花费的最时间。
输入示例
@ -519,7 +519,7 @@ int main() {
所以边添加一次时间复杂度是 O(E) `while (!pq.empty())` 里每次都要弹出一个边来进行操作,在优先级队列(小顶堆)中 弹出一个元素的时间复杂度是 O(logE) ,这是堆排序的时间复杂度。
(当然小顶堆里 是 添加元素的时候 排序,还是 取数元素的时候排序这个无所谓时间复杂度都是O(E),总是一定要排序的,而小顶堆里也不会滞留元素,有多少元素添加 一定就有多少元素弹出)
(当然小顶堆里 是 添加元素的时候 排序,还是 取数元素的时候排序这个无所谓时间复杂度都是O(E),总是一定要排序的,而小顶堆里也不会滞留元素,有多少元素添加 一定就有多少元素弹出)
所以 该算法整体时间复杂度为 OElogE)
@ -537,7 +537,7 @@ int main() {
也行的。
但 正是因为稀疏图,所以我们使用堆优化的思路, 如果我们还用 邻接矩阵 去表达这个图的话,就是 一个高效的算法 使用了低效的数据结构,那么 整体算法效率 依然是低的。
但 正是因为稀疏图,所以我们使用堆优化的思路, 如果我们还用 邻接矩阵 去表达这个图的话,就是 **一个高效的算法 使用了低效的数据结构,那么 整体算法效率 依然是低的**
如果还不清楚为什么要使用 邻接表,可以再看看上面 我在 「图的存储」标题下的讲解。
@ -626,7 +626,7 @@ int main() {
正如我在开篇就给大家交代清楚 堆优化方式的背景。
堆优化的整体思路和 朴素版是大体一样的,区别是 堆优化从边的角度触发,且利用堆来排序。
堆优化的整体思路和 朴素版是大体一样的,区别是 堆优化从边的角度出发且利用堆来排序。
很多录友别说写堆优化 就是看 堆优化的代码也看的很懵。

View File

@ -19,7 +19,7 @@
【输出描述】
输出一个整数,代表小明在途中和其他科学家和科研团队交流所花费的最时间。
输出一个整数,代表小明从起点到终点所花费的最时间。
输入示例

View File

@ -1,7 +1,7 @@
# Bellman_ford 队列优化算法又名SPFA
[卡码网: 94. 城市间货物运输 I](https://kamacoder.com/problempage.php?pid=1152)
[卡码网94. 城市间货物运输 I](https://kamacoder.com/problempage.php?pid=1152)
题目描述
@ -16,6 +16,8 @@
城市 1 到城市 n 之间可能会出现没有路径的情况,同时保证道路网络中不存在任何负权回路。
> 负权回路是指一系列道路的总权值为负,这样的回路使得通过反复经过回路中的道路,理论上可以无限地减少总成本或无限地增加总收益。
输入描述
第一行包含两个正整数,第一个正整数 n 表示该国一共有 n 个城市,第二个整数 m 表示这些城市中共有 m 条道路。
@ -68,7 +70,7 @@
基于以上思路,如何记录 上次松弛的时候更新过的节点呢?
用队列来记录。
用队列来记录。(其实用栈也行,对元素顺序没有要求)
接下来来举例这个队列是如何工作的。
@ -115,7 +117,7 @@
将节点4节点5 加入队列,如图:
![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240411115527.png)
![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240412110348.png)
--------------------
@ -125,7 +127,7 @@
因为没有从节点3作为出发点的边所以这里就从队列里取出节点3就好不用做其他操作如图
![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240411115515.png)
![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240412110420.png)
------------
@ -138,7 +140,7 @@
如图:
![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240411115451.png)
![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240412110445.png)
---------------
@ -151,11 +153,14 @@
如图:
![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240411115436.png)
![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240412110509.png)
因为节点3和 节点6 都曾经加入过队列,不用重复加入,避免重复计算。
在代码中我们可以用一个数组 visited 来记录入过队列的元素,加入过队列的元素,不再重复入队列。
--------------
@ -172,16 +177,16 @@
这样我们就完成了基于队列优化的bellman_ford的算法模拟过程。
大家可以发现 基于队列优化的算法要比bellman_ford 算法 减少很多无用的松弛情况,特别是对于边众多的大图 优化效果明显。
大家可以发现 基于队列优化的算法要比bellman_ford 算法 减少很多无用的松弛情况,特别是对于边众多的大图 优化效果明显。
了解了大体流程,我们再看代码应该怎么写。
在上面模拟过程中,我们每次都要知道 一个节点作为出发点 链接了哪些节点。
如果想方便道这些数据,就需要使用邻接表来存储这个图,如果对于邻接表不了解的话,可以看 [kama0047.参会dijkstra堆](./kama0047.参会dijkstra堆.md) 中 图的存储 部分。
如果想方便道这些数据,就需要使用邻接表来存储这个图,如果对于邻接表不了解的话,可以看 [kama0047.参会dijkstra堆](./kama0047.参会dijkstra堆.md) 中 图的存储 部分。
代码如下:
整体代码如下:
```CPP
#include <iostream>
@ -218,23 +223,18 @@ int main() {
minDist[start] = 0;
queue<int> que;
que.push(start);
int que_size;
que.push(start); // 队列里放入起点
while (!que.empty()) {
// 注意这个数组放的位置
vector<bool> visited(n + 1, false); // 可加,可不加,加了效率高一些,防止队列里重复访问,其数值已经算过了
que_size = que.size();
int node = que.front(); que.pop();
for (Edge edge : grid[node]) {
int from = node;
int to = edge.to;
int price = edge.val;
if (minDist[to] > minDist[from] + price) { // 开始松弛
minDist[to] = minDist[from] + price;
if(visited[to]) continue; // 节点不用重复放入队列,但节点需要重复计算,所以放在这里位置
visited[to] = true;
int value = edge.val;
if (minDist[to] > minDist[from] + value) { // 开始松弛
minDist[to] = minDist[from] + value;
que.push(to);
}
}
@ -244,41 +244,103 @@ int main() {
if (minDist[end] == INT_MAX) cout << "unconnected" << endl; // 不能到达终点
else cout << minDist[end] << endl; // 到达终点最短路径
}
```
代码中有一点需要注意,即 `if(visited[to]) continue;` 这段代码放的位置。
## 效率分析
队列优化版Bellman_ford 的时间复杂度 并不稳定,效率高低依赖于图的结构。
一些录友可能写成这样:
例如 如果是一个双向图,且每一个节点和所有其他节点都相连的话,那么该算法的时间复杂度就接近于 Bellman_ford 的 O(N * E) N 为节点数量E为边的数量。
在这种图中,每一个节点都会重复加入队列 n - 1次因为 这种图中 每个节点 都有 n-1 条指向该节点的边,每条边指向该节点,就需要加入一次队列。(如果这里看不懂,可以在重温一下代码逻辑)
至于为什么 双向图且每一个节点和所有其他节点都相连的话,每个节点 都有 n-1 条指向该节点的边, 我再来举个例子,如图:
[](https://code-thinking-1253855093.file.myqcloud.com/pics/20240416104138.png)
图中 每个节点都与其他所有节点相连节点数n 为 4每个节点都有3条指向该节点的边即入度为3。
n为其他数值的时候也是一样的。
当然这种图是比较极端的情况,也是最稠密的图。
所以如果图越稠密,则 SPFA的效率越接近与 Bellman_ford。
反之图越稀疏SPFA的效率就越高。
一般来说SPFA 的时间复杂度为 O(K * N) K 为不定值,因为 节点需要计入几次队列取决于 图的稠密度。
如果图是一条线形图且单向的话每个节点的入度为1那么只需要加入一次队列这样时间复杂度就是 O(N)。
所以 SPFA 在最坏的情况下是 O(N * E),但 一般情况下 时间复杂度为 O(K * N)。
尽管如此,**以上分析都是 理论上的时间复杂度分析**。
并没有计算 出队列 和 入队列的时间消耗。 因为这个在不同语言上 时间消耗也是不一定的。
以C++为例,以下两端代码理论上,时间复杂度都是 O(n)
```CPP
if (minDist[to] > minDist[from] + price) { // 开始松弛
if(visited[to]) continue;
minDist[to] = minDist[from] + price;
visited[to] = true;
que.push(to);
for (long long i = 0; i < n; i++) {
k++;
}
```
这是不对了,我们仅仅是控制节点不用重复加入队列,但对于边的松弛,节点数值的更新,是要重复计算的,要不然如何 不断更新最短路径呢?
所以 `if(visited[to]) continue;` 应该放在这里:
```CPP
if (minDist[to] > minDist[from] + price) { // 开始松弛
minDist[to] = minDist[from] + price;
if(visited[to]) continue; // 仅仅控制节点不要重复加入队列
visited[to] = true;
que.push(to);
for (long long i = 0; i < n; i++) {
que.push(i);
que.front();
que.pop();
}
```
在 MacBook Pro (13-inch, M1, 2020) 机器上分别测试这两段代码的时间消耗情况:
* n = 10^4第一段代码的时间消耗1ms第二段代码的时间消耗 4 ms
* n = 10^5第一段代码的时间消耗1ms第二段代码的时间消耗 13 ms
* n = 10^6第一段代码的时间消耗4ms第二段代码的时间消耗 59 ms
* n = 10^7第一段代码的时间消耗: 24ms第二段代码的时间消耗 463 ms
* n = 10^8第一段代码的时间消耗: 135ms第二段代码的时间消耗 4268 ms
在这里就可以看出 出队列和入队列 其实也是十分耗时的。
SPFA队列优化版Bellman_ford 在理论上 时间复杂度更胜一筹,但实际上,也要看图的稠密程度,如果 图很大且非常稠密的情况下,虽然 SPFA的时间复杂度接近Bellman_ford但实际时间消耗 可能是 SPFA耗时更多。
针对这种情况,我在后面题目讲解中,会特别加入稠密图的测试用例来给大家讲解。
## 拓展
关于 加visited 方式节点重复方便,可能有录友认为,加上 visited 也是防止 如果图中出现了环的话,会导致的 队列里一直不为空。
这里可能有录友疑惑,`while (!que.empty())` 队里里 会不会造成死循环? 例如 图中有环,这样一直有元素加入到队列里?
其实有环的情况,要看它是 正权回路 还是 负全回路。
题目描述中,已经说了,本题没有 负权回路 。
如图:
![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240412111849.png)
正权回路 就是有环,但环的总权值为正数。
在有环且只有正权回路的情况下,即使元素重复加入队列,最后,也会因为 所有边都松弛后节点数值minDist数组不在发生变化了 而终止。
(而且有重复元素加入队列是正常的,多条路径到达同一个节点,节点必要要选择一个最短的路径,而这个节点就会重复加入队列进行判断,选一个最短的)
在[0094.城市间货物运输I](./0094.城市间货物运输I.md) 中我们讲过对所有边 最多松弛 n -1 次,就一定可以求出所有起点到所有节点的最小距离即 minDist数组。
即使再松弛n次以上 所有起点到所有节点的最小距离minDist数组 不会再变了。 (这里如果不理解,建议认真看[0094.城市间货物运输I](./0094.城市间货物运输I.md)讲解)
所以本题我们使用队列优化,有元素重复加入队列,也会因为最后 minDist数组 不会在发生变化而终止。
节点再加入队列,需要有松弛的行为, 而 每个节点已经都计算出来 起点到该节点的最短路径,那么就不会有 执行这个判断条件`if (minDist[to] > minDist[from] + value)`,从而不会有新的节点加入到队列。
但如果本题有 负权回路,那情况就不一样了,我在下一题目讲解中,会重点讲解 负权回路 带来的变化。

View File

@ -16,6 +16,8 @@
城市 1 到城市 n 之间可能会出现没有路径的情况,同时保证道路网络中不存在任何负权回路。
> 负权回路是指一系列道路的总权值为负,这样的回路使得通过反复经过回路中的道路,理论上可以无限地减少总成本或无限地增加总收益。
输入描述
第一行包含两个正整数,第一个正整数 n 表示该国一共有 n 个城市,第二个整数 m 表示这些城市中共有 m 条道路。
@ -55,6 +57,7 @@
**Bellman_ford算法的核心思想是 对所有边进行松弛n-1次操作n为节点数量从而求得目标最短路**。
## 什么叫做松弛
看到这里,估计大家都比较晕了,为什么是 n-1 次,那“松弛”这两个字究竟是个啥意思?

View File

@ -11,7 +11,7 @@
权值为负则表示政府的补贴超过了支出的运输成本,实际表现为运输过程中还能赚取一定的收益。
然而,在评估从城市 1 到城市 n 的所有可能路径中综合政府补贴后的最低运输成本时,存在一种情况:图中可能出现负权回路。
然而,在评估从城市 1 到城市 n 的所有可能路径中综合政府补贴后的最低运输成本时,存在一种情况:**图中可能出现负权回路**
负权回路是指一系列道路的总权值为负,这样的回路使得通过反复经过回路中的道路,理论上可以无限地减少总成本或无限地增加总收益。
@ -151,3 +151,90 @@ int main() {
* 时间复杂度: O(N * E) , N为节点数量E为图中边的数量
* 空间复杂度: O(N) ,即 minDist 数组所开辟的空间
## 拓展
本题可不可 使用 队列优化版的bellman_fordSPFA
上面的解法中我们对所有边松弛了n-1次后在松弛一次如果出现minDist出现变化就判断有负权回路。
如果使用 SPFA 那么节点都是进队列的,那么节点进入队列几次后 足够判断该图是否有负权回路呢?
在 [0094.城市间货物运输I-SPFA](./0094.城市间货物运输I-SPFA) 中,我们讲过 在极端情况下,即:所有节点都与其他节点相连,每个节点的入度为 n-1 n为节点数量所以每个节点最多加入 n-1 次队列。
那么如果节点加入队列的次数 超过了 n-1次 ,那么该图就一定有负权回路。
所以本题也是可以使用 SPFA 来做的。 代码如下:
```CPP
#include <iostream>
#include <vector>
#include <queue>
#include <list>
#include <climits>
using namespace std;
struct Edge { //邻接表
int to; // 链接的节点
int val; // 边的权重
Edge(int t, int w): to(t), val(w) {} // 构造函数
};
int main() {
int n, m, p1, p2, val;
cin >> n >> m;
vector<list<Edge>> grid(n + 1); // 邻接表
// 将所有边保存起来
for(int i = 0; i < m; i++){
cin >> p1 >> p2 >> val;
// p1 指向 p2权值为 val
grid[p1].push_back(Edge(p2, val));
}
int start = 1; // 起点
int end = n; // 终点
vector<int> minDist(n + 1 , INT_MAX);
minDist[start] = 0;
queue<int> que;
que.push(start); // 队列里放入起点
vector<int> count(n+1, 0); // 记录节点加入队列几次
count[start]++;
bool flag = false;
while (!que.empty()) {
int node = que.front(); que.pop();
for (Edge edge : grid[node]) {
int from = node;
int to = edge.to;
int value = edge.val;
if (minDist[to] > minDist[from] + value) { // 开始松弛
minDist[to] = minDist[from] + value;
que.push(to);
count[to]++;
if (count[to] == n) {// 如果加入队列次数超过 n-1次 就说明该图与负权回路
flag = true;
while (!que.empty()) que.pop();
break;
}
}
}
}
if (flag) cout << "circle" << endl;
else if (minDist[end] == INT_MAX) {
cout << "unconnected" << endl;
} else {
cout << minDist[end] << endl;
}
}
```

View File

@ -65,7 +65,7 @@
图中节点2 最多已经经过2个节点 到达节点4那么中间是有多少条边呢是 3 条边对吧。
所以本题就是求起点最多经过k + 1 条边到达终点的最短距离。
所以本题就是求起点最多经过k + 1 条边到达终点的最短距离。
对所有边松弛一次,相当于计算 起点到达 与起点一条边相连的节点 的最短距离,那么对所有边松弛 k + 1次就是求 起点到达 与起点k + 1条边相连的节点的 最短距离。
@ -339,7 +339,7 @@ int main() {
其实这是和示例中给出的边的顺序是有关的,
我们按照修改后的示例再来模拟 对所有边的第一次拓展情况。
我们按照修改后的示例再来模拟 对所有边的第一次拓展情况。
初始化:
@ -366,20 +366,17 @@ int main() {
那么前面讲解过的 [94.城市间货物运输I](./kama94.城市间货物运输I.md) 和 [95.城市间货物运输II](./kama95.城市间货物运输II.md) 也是bellman_ford经典算法也没使用 minDist_copy怎么就没问题呢
> 如果没看过我上面这两篇讲解的话,建议详细学习上面两篇,看我下面讲的区别,否则容易看不懂。
> 如果没看过我上面这两篇讲解的话,建议详细学习上面两篇,看我下面讲的区别,否则容易看不懂。
[94.城市间货物运输I](./kama94.城市间货物运输I.md) 是没有 负权回路的,那么 多松弛多少次,对结果都没有影响。
求 节点1 到 节点n 的最短路径松弛n-1 次就够了,松弛 大于 n-1次结果也不会变。
那么在对所有边进行第一次松弛的时候,如果基于 最近计算的 minDist 来计算 minDist (相当于多做松弛了),也是对最终结果没影响。
那么在对所有边进行第一次松弛的时候,如果基于 本次计算的 minDist 来计算 minDist (相当于多做松弛了),也是对最终结果没影响。
[95.城市间货物运输II](./kama95.城市间货物运输II.md) 是判断是否有 负权回路,一旦有负权回路, 对所有边松弛 n -1 次以后,在做松弛 minDist 数值一定会变,根据这一点判断是否有负权回路。
所以 在对所有边进行第一次松弛的时候,如果基于 最近计算的 minDist 来计算 minDist (相当于多做松弛了),对最后判断是否有负权回路同样没有影响。
你可以理解 minDist的数组其实是不准确了但它只要变化了就可以让我们来判断 是否有 负权回路。
[95.城市间货物运输II](./kama95.城市间货物运输II.md) 是判断是否有 负权回路,一旦有负权回路, 对所有边松弛 n-1 次以后,在做松弛 minDist 数值一定会变,根据这一点判断是否有负权回路。
所以,[95.城市间货物运输II](./kama95.城市间货物运输II.md) 只需要判断minDist数值变化了就行而 minDist 的数值对不对,并不是我们关心的。
那么本题 为什么计算minDist 一定要基于上次 的 minDist 数值。
@ -390,3 +387,199 @@ int main() {
如果本题中 没有负权回路的测试用例, 那版本一的代码就可以过了,也就不用我费这么大口舌去讲解的这个坑了。
## 拓展三SPFA
本题也可以用 SPFA来做关于 SPFA ,已经在这里 [0094.城市间货物运输I-SPFA](./0094.城市间货物运输I-SPFA.md) 有详细讲解。
使用SPFA算法解决本题的时候关键在于 如何控制松弛k次。
其实实现不难,但有点技巧,可以用一个变量 que_size 记录每一轮松弛入队列的所有节点数量。
下一轮松弛的时候,就把队列里 que_size 个节点都弹出来,就是上一轮松弛入队列的节点。
代码如下(详细注释)
```CPP
#include <iostream>
#include <vector>
#include <queue>
#include <list>
#include <climits>
using namespace std;
struct Edge { //邻接表
int to; // 链接的节点
int val; // 边的权重
Edge(int t, int w): to(t), val(w) {} // 构造函数
};
int main() {
int n, m, p1, p2, val;
cin >> n >> m;
vector<list<Edge>> grid(n + 1); // 邻接表
// 将所有边保存起来
for(int i = 0; i < m; i++){
cin >> p1 >> p2 >> val;
// p1 指向 p2权值为 val
grid[p1].push_back(Edge(p2, val));
}
int start, end, k;
cin >> start >> end >> k;
k++;
vector<int> minDist(n + 1 , INT_MAX);
vector<int> minDist_copy(n + 1); // 用来记录每一次遍历的结果
minDist[start] = 0;
queue<int> que;
que.push(start); // 队列里放入起点
int que_size;
while (k-- && !que.empty()) {
minDist_copy = minDist; // 获取上一次计算的结果
que_size = que.size(); // 记录上次入队列的节点个数
while (que_size--) { // 上一轮松弛入队列的节点,这次对应的边都要做松弛
int node = que.front(); que.pop();
for (Edge edge : grid[node]) {
int from = node;
int to = edge.to;
int price = edge.val;
if (minDist[to] > minDist_copy[from] + price) {
minDist[to] = minDist_copy[from] + price;
que.push(to);
}
}
}
}
if (minDist[end] == INT_MAX) cout << "unreachable" << endl;
else cout << minDist[end] << endl;
}
```
时间复杂度: O(K * H) H 为不确定数,取决于 图的稠密度但H 一定是小于等于 E 的
关于 SPFA的是时间复杂度分析我在[0094.城市间货物运输I-SPFA](./0094.城市间货物运输I-SPFA.md) 有详细讲解
但大家会发现,以上代码大家提交后,怎么耗时这么多?
![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240418113308.png)
理论上SPFA的时间复杂度不是要比 bellman_ford 更优吗?
怎么耗时多了这么多呢?
以上代码有一个可以改进的点,每一轮松弛中,重复节点可以不用入队列。
因为重复节点入队列,下次从队列里取节点的时候,该节点要取很多次,而且都是重复计算。
所以代码可以优化成这样:
```CPP
#include <iostream>
#include <vector>
#include <queue>
#include <list>
#include <climits>
using namespace std;
struct Edge { //邻接表
int to; // 链接的节点
int val; // 边的权重
Edge(int t, int w): to(t), val(w) {} // 构造函数
};
int main() {
int n, m, p1, p2, val;
cin >> n >> m;
vector<list<Edge>> grid(n + 1); // 邻接表
// 将所有边保存起来
for(int i = 0; i < m; i++){
cin >> p1 >> p2 >> val;
// p1 指向 p2权值为 val
grid[p1].push_back(Edge(p2, val));
}
int start, end, k;
cin >> start >> end >> k;
k++;
vector<int> minDist(n + 1 , INT_MAX);
vector<int> minDist_copy(n + 1); // 用来记录每一次遍历的结果
minDist[start] = 0;
queue<int> que;
que.push(start); // 队列里放入起点
int que_size;
while (k-- && !que.empty()) {
vector<bool> visited(n + 1, false); // 每一轮松弛中,控制节点不用重复入队列
minDist_copy = minDist;
que_size = que.size();
while (que_size--) {
int node = que.front(); que.pop();
for (Edge edge : grid[node]) {
int from = node;
int to = edge.to;
int price = edge.val;
if (minDist[to] > minDist_copy[from] + price) {
minDist[to] = minDist_copy[from] + price;
if(visited[to]) continue; // 不用重复放入队列,但需要重复松弛,所以放在这里位置
visited[to] = true;
que.push(to);
}
}
}
}
if (minDist[end] == INT_MAX) cout << "unreachable" << endl;
else cout << minDist[end] << endl;
}
```
以上代码提交后,耗时情况:
![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240418113952.png)
大家发现 依然远比 bellman_ford 的代码版本 耗时高。
这又是为什么呢?
可以发现耗时主要是在 第8组数据上
![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240418114511.png)
其实第八组数据是我特别制作的一个 稠密大图该图有250个节点和10000条边 在这种情况下, SPFA 的时间复杂度 是接近与 bellman_ford的。
但因为 SPFA 节点的进出队列操作耗时很大所以相同的时间复杂度的情况下SPFA 实际上更耗时了。
这一点我在 [0094.城市间货物运输I-SPFA](./0094.城市间货物运输I-SPFA.md) 有分析,感兴趣的录友再回头去看看。
## 总结
本题是单源有限最短路问题,也是 bellman_ford的一个拓展问题如果理解bellman_ford 其实思路比较容易理解,但有很多细节。
例如 为什么要用 minDist_copy 来记录上一轮 松弛的结果。 这也是本篇我为什么花了这么大篇幅讲解的关键所在。
接下来,还给大家多了三个拓展:
* 边的顺序的影响
* 本题的本质
* SPFA的解法
学透了以上三个拓展相信大家会对bellman_ford有更深入的理解。

View File

@ -0,0 +1,367 @@
# 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] 来表示。
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 的最小距离了。
**初始化这里要画图,对后面的遍历顺序理解很重要**
所以初始化:
```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://code-thinking-1253855093.file.myqcloud.com/pics/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://code-thinking-1253855093.file.myqcloud.com/pics/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://code-thinking-1253855093.file.myqcloud.com/pics/20240424120942.png)
就节点1 到 节点 2 的最短距离,运行结果是 10 但正确的结果很明显是3。
为什么呢?
因为 k 放在最里面,先就把 节点1 和 节点 2 的最短距离就确定了,后面再也不会计算节点 1 和 节点 2的距离同时也不会基于 初始化或者之前计算过的结果来计算,即不会考虑 节点1 到 节点3 节点3 到节点 4节点4到节点2 的距离。
而遍历k 的for循环如果放在中间呢同样是 j 与k 行程一个平面i 是纵面,遍历的也是这样:
![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240424115827.png)
同样不能完全用上初始化 和 上一层计算的结果。
很多录友对于 floyd算法的遍历顺序搞不懂其实 是没有从三维的角度去思考,同时我把三维立体图给大家画出来,遍历顺序标出来,大家就很容易想明白,为什么 k 放在最外层 才能用上 初始化和上一轮计算的结果了。
```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;
}
}
```
# 拓展 负权回路
本题可以有负数,但不能出现负权回路
---------
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/
-----------
```CPP
#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;
}
}
```