mirror of
https://github.com/youngyangyang04/leetcode-master.git
synced 2025-07-10 12:15:58 +08:00
Update
This commit is contained in:
@ -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))
|
||||
|
@ -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 的路径长度为 4,2 到 3 之间并没有道路。
|
||||
|
||||
1 <= N, M, Q <= 1000.
|
||||
|
||||
## 思路
|
||||
|
||||
本题是经典的多源最短路问题。
|
||||
|
||||
我们之前讲解过的算法,dijkstra,
|
||||
|
||||
|
@ -19,7 +19,7 @@
|
||||
|
||||
【输出描述】
|
||||
|
||||
输出一个整数,代表小明在途中和其他科学家和科研团队交流所花费的最少时间。
|
||||
输出一个整数,代表小明从起点到终点所花费的最小时间。
|
||||
|
||||
输入示例
|
||||
|
||||
@ -519,7 +519,7 @@ int main() {
|
||||
|
||||
所以边添加一次时间复杂度是 O(E), `while (!pq.empty())` 里每次都要弹出一个边来进行操作,在优先级队列(小顶堆)中 弹出一个元素的时间复杂度是 O(logE) ,这是堆排序的时间复杂度。
|
||||
|
||||
(当然小顶堆里 是 添加元素的时候 排序,还是 取数元素的时候排序,这个无所谓,时间复杂度都是O(E),总是是一定要排序的,而小顶堆里也不会滞留元素,有多少元素添加 一定就有多少元素弹出)
|
||||
(当然小顶堆里 是 添加元素的时候 排序,还是 取数元素的时候排序,这个无所谓,时间复杂度都是O(E),总之是一定要排序的,而小顶堆里也不会滞留元素,有多少元素添加 一定就有多少元素弹出)
|
||||
|
||||
所以 该算法整体时间复杂度为 O(ElogE)
|
||||
|
||||
@ -537,7 +537,7 @@ int main() {
|
||||
|
||||
也行的。
|
||||
|
||||
但 正是因为稀疏图,所以我们使用堆优化的思路, 如果我们还用 邻接矩阵 去表达这个图的话,就是 一个高效的算法 使用了低效的数据结构,那么 整体算法效率 依然是低的。
|
||||
但 正是因为稀疏图,所以我们使用堆优化的思路, 如果我们还用 邻接矩阵 去表达这个图的话,就是 **一个高效的算法 使用了低效的数据结构,那么 整体算法效率 依然是低的**。
|
||||
|
||||
如果还不清楚为什么要使用 邻接表,可以再看看上面 我在 「图的存储」标题下的讲解。
|
||||
|
||||
@ -626,7 +626,7 @@ int main() {
|
||||
|
||||
正如我在开篇就给大家交代清楚 堆优化方式的背景。
|
||||
|
||||
堆优化的整体思路和 朴素版是大体一样的,区别是 堆优化从边的角度触发,且利用堆来排序。
|
||||
堆优化的整体思路和 朴素版是大体一样的,区别是 堆优化从边的角度出发且利用堆来排序。
|
||||
|
||||
很多录友别说写堆优化 就是看 堆优化的代码也看的很懵。
|
||||
|
@ -19,7 +19,7 @@
|
||||
|
||||
【输出描述】
|
||||
|
||||
输出一个整数,代表小明在途中和其他科学家和科研团队交流所花费的最少时间。
|
||||
输出一个整数,代表小明从起点到终点所花费的最小时间。
|
||||
|
||||
输入示例
|
||||
|
@ -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 加入队列,如图:
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
--------------------
|
||||
@ -125,7 +127,7 @@
|
||||
|
||||
因为没有从节点3作为出发点的边,所以这里就从队列里取出节点3就好,不用做其他操作,如图:
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
------------
|
||||
@ -138,7 +140,7 @@
|
||||
|
||||
如图:
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
---------------
|
||||
@ -151,10 +153,13 @@
|
||||
|
||||
如图:
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
因为节点3,和 节点6 都曾经加入过队列,不用重复加入,避免重复计算。
|
||||
|
||||
因为节点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,24 +223,19 @@ 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;
|
||||
que.push(to);
|
||||
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())` 队里里 会不会造成死循环? 例如 图中有环,这样一直有元素加入到队列里?
|
||||
|
||||
其实有环的情况,要看它是 正权回路 还是 负全回路。
|
||||
|
||||
题目描述中,已经说了,本题没有 负权回路 。
|
||||
|
||||
如图:
|
||||
|
||||

|
||||
|
||||
正权回路 就是有环,但环的总权值为正数。
|
||||
|
||||
在有环且只有正权回路的情况下,即使元素重复加入队列,最后,也会因为 所有边都松弛后,节点数值(minDist数组)不在发生变化了 而终止。
|
||||
|
||||
(而且有重复元素加入队列是正常的,多条路径到达同一个节点,节点必要要选择一个最短的路径,而这个节点就会重复加入队列进行判断,选一个最短的)
|
||||
|
||||
在[0094.城市间货物运输I](./0094.城市间货物运输I.md) 中我们讲过对所有边 最多松弛 n -1 次,就一定可以求出所有起点到所有节点的最小距离即 minDist数组。
|
||||
|
||||
即使再松弛n次以上, 所有起点到所有节点的最小距离(minDist数组) 不会再变了。 (这里如果不理解,建议认真看[0094.城市间货物运输I](./0094.城市间货物运输I.md)讲解)
|
||||
|
||||
所以本题我们使用队列优化,有元素重复加入队列,也会因为最后 minDist数组 不会在发生变化而终止。
|
||||
|
||||
节点再加入队列,需要有松弛的行为, 而 每个节点已经都计算出来 起点到该节点的最短路径,那么就不会有 执行这个判断条件`if (minDist[to] > minDist[from] + value)`,从而不会有新的节点加入到队列。
|
||||
|
||||
但如果本题有 负权回路,那情况就不一样了,我在下一题目讲解中,会重点讲解 负权回路 带来的变化。
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -16,6 +16,8 @@
|
||||
|
||||
城市 1 到城市 n 之间可能会出现没有路径的情况,同时保证道路网络中不存在任何负权回路。
|
||||
|
||||
> 负权回路是指一系列道路的总权值为负,这样的回路使得通过反复经过回路中的道路,理论上可以无限地减少总成本或无限地增加总收益。
|
||||
|
||||
输入描述
|
||||
|
||||
第一行包含两个正整数,第一个正整数 n 表示该国一共有 n 个城市,第二个整数 m 表示这些城市中共有 m 条道路。
|
||||
@ -55,6 +57,7 @@
|
||||
|
||||
**Bellman_ford算法的核心思想是 对所有边进行松弛n-1次操作(n为节点数量),从而求得目标最短路**。
|
||||
|
||||
## 什么叫做松弛
|
||||
|
||||
看到这里,估计大家都比较晕了,为什么是 n-1 次,那“松弛”这两个字究竟是个啥意思?
|
||||
|
@ -11,7 +11,7 @@
|
||||
|
||||
权值为负则表示政府的补贴超过了支出的运输成本,实际表现为运输过程中还能赚取一定的收益。
|
||||
|
||||
然而,在评估从城市 1 到城市 n 的所有可能路径中综合政府补贴后的最低运输成本时,存在一种情况:图中可能出现负权回路。
|
||||
然而,在评估从城市 1 到城市 n 的所有可能路径中综合政府补贴后的最低运输成本时,存在一种情况:**图中可能出现负权回路**。
|
||||
|
||||
负权回路是指一系列道路的总权值为负,这样的回路使得通过反复经过回路中的道路,理论上可以无限地减少总成本或无限地增加总收益。
|
||||
|
||||
@ -151,3 +151,90 @@ int main() {
|
||||
|
||||
* 时间复杂度: O(N * E) , N为节点数量,E为图中边的数量
|
||||
* 空间复杂度: O(N) ,即 minDist 数组所开辟的空间
|
||||
|
||||
## 拓展
|
||||
|
||||
本题可不可 使用 队列优化版的bellman_ford(SPFA)呢?
|
||||
|
||||
上面的解法中,我们对所有边松弛了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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
```
|
@ -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) 有详细讲解
|
||||
|
||||
但大家会发现,以上代码大家提交后,怎么耗时这么多?
|
||||
|
||||

|
||||
|
||||
理论上,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;
|
||||
}
|
||||
```
|
||||
|
||||
以上代码提交后,耗时情况:
|
||||
|
||||

|
||||
|
||||
大家发现 依然远比 bellman_ford 的代码版本 耗时高。
|
||||
|
||||
这又是为什么呢?
|
||||
|
||||
可以发现耗时主要是在 第8组数据上:
|
||||
|
||||

|
||||
|
||||
其实第八组数据是我特别制作的一个 稠密大图,该图有250个节点和10000条边, 在这种情况下, SPFA 的时间复杂度 是接近与 bellman_ford的。
|
||||
|
||||
但因为 SPFA 节点的进出队列操作,耗时很大,所以相同的时间复杂度的情况下,SPFA 实际上更耗时了。
|
||||
|
||||
这一点我在 [0094.城市间货物运输I-SPFA](./0094.城市间货物运输I-SPFA.md) 有分析,感兴趣的录友再回头去看看。
|
||||
|
||||
## 总结
|
||||
|
||||
本题是单源有限最短路问题,也是 bellman_ford的一个拓展问题,如果理解bellman_ford 其实思路比较容易理解,但有很多细节。
|
||||
|
||||
例如 为什么要用 minDist_copy 来记录上一轮 松弛的结果。 这也是本篇我为什么花了这么大篇幅讲解的关键所在。
|
||||
|
||||
接下来,还给大家多了三个拓展:
|
||||
|
||||
* 边的顺序的影响
|
||||
* 本题的本质
|
||||
* SPFA的解法
|
||||
|
||||
学透了以上三个拓展,相信大家会对bellman_ford有更深入的理解。
|
367
problems/kamacoder/0097.小明逛公园.md
Normal file
367
problems/kamacoder/0097.小明逛公园.md
Normal 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 的路径长度为 4,2 到 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] = 3,k需要填什么呢?
|
||||
|
||||
把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循环,分别遍历i,j 和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 循环的先后顺序无所谓。
|
||||
|
||||
代码如下:
|
||||
|
||||
```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 则是纵面,那遍历 就是这样的:
|
||||
|
||||

|
||||
|
||||
|
||||
而我们初始化,是 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 放在最外层 才能用上 初始化和上一轮计算的结果了。
|
||||
|
||||
|
||||
|
||||
|
||||
```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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
|
Reference in New Issue
Block a user