Skip to content

Commit

Permalink
Update
Browse files Browse the repository at this point in the history
  • Loading branch information
youngyangyang04 committed Apr 24, 2024
1 parent a6ba82a commit 956e853
Show file tree
Hide file tree
Showing 13 changed files with 768 additions and 113 deletions.
6 changes: 3 additions & 3 deletions problems/0279.完全平方数.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ class Solution {

### Python:

先遍历物品, 再遍历背包
先遍历背包, 再遍历物品
```python
class Solution:
def numSquares(self, n: int) -> int:
Expand All @@ -234,7 +234,7 @@ class Solution:
return dp[n]

```
先遍历背包, 再遍历物品
先遍历物品, 再遍历背包
```python
class Solution:
def numSquares(self, n: int) -> int:
Expand Down Expand Up @@ -389,7 +389,7 @@ function numSquares(n: number): number {
};
```

## C
### C

```c
#define min(a, b) ((a) > (b) ? (b) : (a))
Expand Down
57 changes: 0 additions & 57 deletions problems/kama0097.小明逛公园.md

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

【输出描述】

输出一个整数,代表小明在途中和其他科学家和科研团队交流所花费的最少时间
输出一个整数,代表小明从起点到终点所花费的最小时间

输入示例

Expand Down Expand Up @@ -519,7 +519,7 @@ int main() {
所以边添加一次时间复杂度是 O(E), `while (!pq.empty())` 里每次都要弹出一个边来进行操作,在优先级队列(小顶堆)中 弹出一个元素的时间复杂度是 O(logE) ,这是堆排序的时间复杂度。
(当然小顶堆里 是 添加元素的时候 排序,还是 取数元素的时候排序,这个无所谓,时间复杂度都是O(E),总是是一定要排序的,而小顶堆里也不会滞留元素,有多少元素添加 一定就有多少元素弹出)
(当然小顶堆里 是 添加元素的时候 排序,还是 取数元素的时候排序,这个无所谓,时间复杂度都是O(E),总之是一定要排序的,而小顶堆里也不会滞留元素,有多少元素添加 一定就有多少元素弹出)
所以 该算法整体时间复杂度为 O(ElogE)
Expand All @@ -537,7 +537,7 @@ int main() {
也行的。
但 正是因为稀疏图,所以我们使用堆优化的思路, 如果我们还用 邻接矩阵 去表达这个图的话,就是 一个高效的算法 使用了低效的数据结构,那么 整体算法效率 依然是低的。
但 正是因为稀疏图,所以我们使用堆优化的思路, 如果我们还用 邻接矩阵 去表达这个图的话,就是 **一个高效的算法 使用了低效的数据结构,那么 整体算法效率 依然是低的**
如果还不清楚为什么要使用 邻接表,可以再看看上面 我在 「图的存储」标题下的讲解。
Expand Down Expand Up @@ -626,7 +626,7 @@ int main() {

正如我在开篇就给大家交代清楚 堆优化方式的背景。

堆优化的整体思路和 朴素版是大体一样的,区别是 堆优化从边的角度触发,且利用堆来排序
堆优化的整体思路和 朴素版是大体一样的,区别是 堆优化从边的角度出发且利用堆来排序

很多录友别说写堆优化 就是看 堆优化的代码也看的很懵。

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

【输出描述】

输出一个整数,代表小明在途中和其他科学家和科研团队交流所花费的最少时间
输出一个整数,代表小明从起点到终点所花费的最小时间

输入示例

Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -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)

题目描述

Expand All @@ -16,6 +16,8 @@

城市 1 到城市 n 之间可能会出现没有路径的情况,同时保证道路网络中不存在任何负权回路。

> 负权回路是指一系列道路的总权值为负,这样的回路使得通过反复经过回路中的道路,理论上可以无限地减少总成本或无限地增加总收益。
输入描述

第一行包含两个正整数,第一个正整数 n 表示该国一共有 n 个城市,第二个整数 m 表示这些城市中共有 m 条道路。
Expand Down Expand Up @@ -68,7 +70,7 @@

基于以上思路,如何记录 上次松弛的时候更新过的节点呢?

用队列来记录。
用队列来记录。(其实用栈也行,对元素顺序没有要求)

接下来来举例这个队列是如何工作的。

Expand Down Expand Up @@ -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)


--------------------
Expand All @@ -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)


------------
Expand All @@ -138,7 +140,7 @@

如图:

![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240411115451.png)
![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240412110445.png)


---------------
Expand All @@ -151,10 +153,13 @@

如图:

![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240411115436.png)
![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240412110509.png)



因为节点3,和 节点6 都曾经加入过队列,不用重复加入,避免重复计算。

因为节点3,和 节点6 都曾经加入过队列,不用重复加入,避免重复计算。
在代码中我们可以用一个数组 visited 来记录入过队列的元素,加入过队列的元素,不再重复入队列。


--------------
Expand All @@ -172,16 +177,16 @@

这样我们就完成了基于队列优化的bellman_ford的算法模拟过程。

大家可以发现 基于队列优化的算法,要比bellman_ford 算法 减少很多无用的松弛情况,特别是对于边树众多的大图 优化效果明显。
大家可以发现 基于队列优化的算法,要比bellman_ford 算法 减少很多无用的松弛情况,特别是对于边数众多的大图 优化效果明显。

了解了大体流程,我们再看代码应该怎么写。

在上面模拟过程中,我们每次都要知道 一个节点作为出发点 链接了哪些节点。

如果想方便这道这些数据,就需要使用邻接表来存储这个图,如果对于邻接表不了解的话,可以看 [kama0047.参会dijkstra堆](./kama0047.参会dijkstra堆.md) 中 图的存储 部分。
如果想方便知道这些数据,就需要使用邻接表来存储这个图,如果对于邻接表不了解的话,可以看 [kama0047.参会dijkstra堆](./kama0047.参会dijkstra堆.md) 中 图的存储 部分。


代码如下
整体代码如下

```CPP
#include <iostream>
Expand Down Expand Up @@ -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);
}
}

Expand All @@ -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)`,从而不会有新的节点加入到队列。

但如果本题有 负权回路,那情况就不一样了,我在下一题目讲解中,会重点讲解 负权回路 带来的变化。





Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

城市 1 到城市 n 之间可能会出现没有路径的情况,同时保证道路网络中不存在任何负权回路。

> 负权回路是指一系列道路的总权值为负,这样的回路使得通过反复经过回路中的道路,理论上可以无限地减少总成本或无限地增加总收益。
输入描述

第一行包含两个正整数,第一个正整数 n 表示该国一共有 n 个城市,第二个整数 m 表示这些城市中共有 m 条道路。
Expand Down Expand Up @@ -55,6 +57,7 @@
**Bellman_ford算法的核心思想是 对所有边进行松弛n-1次操作(n为节点数量),从而求得目标最短路**

## 什么叫做松弛

看到这里,估计大家都比较晕了,为什么是 n-1 次,那“松弛”这两个字究竟是个啥意思?

Expand Down
Loading

0 comments on commit 956e853

Please sign in to comment.