diff --git a/Contents/08.Graph/04.Graph-Shortest-Path/01.Graph-Single-Source-Shortest-Path-01.md b/Contents/08.Graph/04.Graph-Shortest-Path/01.Graph-Single-Source-Shortest-Path-01.md index 6152ecb6..ecb599ad 100644 --- a/Contents/08.Graph/04.Graph-Shortest-Path/01.Graph-Single-Source-Shortest-Path-01.md +++ b/Contents/08.Graph/04.Graph-Shortest-Path/01.Graph-Single-Source-Shortest-Path-01.md @@ -1,4 +1,4 @@ -## 1. 单源最短路径的定义 +## 1. 单源最短路径简介 > **单源最短路径(Single Source Shortest Path)**:对于一个带权图 $G = (V, E)$,其中每条边的权重是一个实数。另外,给定 $v$ 中的一个顶点,称之为源点。则源点到其他所有各个顶点之间的最短路径长度,称为单源最短路径。 @@ -20,34 +20,185 @@ > **Dijkstra 算法的算法思想**:通过逐步选择距离起始节点最近的节点,并根据这些节点的路径更新其他节点的距离,从而逐步找到最短路径。 +Dijkstra 算法是一种用来解决单源最短路径问题的算法。这个算法适用于没有负权边的图。算法的核心思想是从源点出发,逐步找到到其他所有点的最短路径。它通过不断选择当前距离源点最近的节点,并更新与该节点相邻的节点的距离,最终得到所有节点的最短路径。 + +Dijkstra 算法使用贪心的策略。它每次选择当前未处理的节点中距离源点最近的节点,认为这个节点的最短路径已经确定。然后,它用这个节点的最短路径去更新其他相邻节点的距离。这个过程重复进行,直到所有节点的最短路径都被确定。 + +Dijkstra 算法的一个重要特点是它不能处理有负权边的图。因为负权边可能导致已经确定的最短路径被破坏。如果图中存在负权边,应该使用 Bellman-Ford 算法或 SPFA 算法。 + ### 2.2 Dijkstra 算法的实现步骤 +1. 初始化距离数组,将源节点 $source$ 的距离设为 $0$,其他节点的距离设为无穷大。 +2. 维护一个访问数组 $visited$,记录节点是否已经被访问。 +3. 每次从未访问的节点中找到距离最小的节点,标记为已访问。 +4. 更新该节点的所有相邻节点的距离。 +5. 重复步骤 $3 \sim 4$,直到所有节点都被访问。 +6. 最后返回所有节点中最大的距离值,如果存在无法到达的节点则返回 $-1$。 + + + ### 2.3 Dijkstra 算法的实现代码 ```python - +class Solution: + def dijkstra(self, graph, n, source): + # 初始化距离数组 + dist = [float('inf') for _ in range(n + 1)] + dist[source] = 0 + # 记录已处理的节点 + visited = set() + + while len(visited) < n: + # 选择当前未处理的、距离源点最近的节点 + current_node = None + min_distance = float('inf') + for i in range(1, n + 1): + if i not in visited and dist[i] < min_distance: + min_distance = dist[i] + current_node = i + + # 如果没有可处理的节点(非连通图),提前结束 + if current_node is None: + break + + # 标记当前节点为已处理 + visited.add(current_node) + + # 更新相邻节点的距离 + for neighbor, weight in graph[current_node].items(): + new_distance = dist[current_node] + weight + if new_distance < dist[neighbor]: + dist[neighbor] = new_distance + + return dist + +# 使用示例 +# 创建一个有向图,使用邻接表表示 +graph = { + 1: {2: 2, 3: 4}, + 2: {3: 1, 4: 7}, + 3: {4: 3}, + 4: {} +} +n = 4 # 图中节点数量 +source = 1 # 源节点 + +dist = Solution().dijkstra(graph, n, source) +print("从节点", source, "到其他节点的最短距离:") +for i in range(1, n + 1): + if dist[i] == float('inf'): + print(f"到节点 {i} 的距离:不可达") + else: + print(f"到节点 {i} 的距离:{dist[i]}") ``` -## 3. Bellman-Ford 算法 +### 2.4 Dijkstra 算法复杂度分析 -### 3.1 Bellman-Ford 算法的算法思想 +- **时间复杂度**:$O(V^2)$ + - 外层循环需要遍历所有节点,时间复杂度为 $O(V)$ + - 内层循环需要遍历所有未访问的节点来找到距离最小的节点,时间复杂度为 $O(V)$ + - 因此总时间复杂度为 $O(V^2)$ -### 3.2 Bellman-Ford 算法的实现步骤 +- **空间复杂度**:$O(V)$ + - 需要存储距离数组 `dist`,大小为 $O(V)$ + - 需要存储访问集合 `visited`,大小为 $O(V)$ + - 因此总空间复杂度为 $O(V)$ -### 3.3 Bellman-Ford 算法的实现代码 -```python +## 3. 堆优化的 Dijkstra 算法 -``` +### 3.1 堆优化的 Dijkstra 算法思想 + +> **堆优化的 Dijkstra 算法**:通过使用优先队列(堆)来优化选择最小距离节点的过程,从而降低算法的时间复杂度。 -## 4. SPFA 算法 +在原始的 Dijkstra 算法中,每次都需要遍历所有未访问的节点来找到距离最小的节点,这个过程的时间复杂度是 $O(V)$。通过使用优先队列(堆)来维护当前已知的最短距离,我们可以将这个过程的时间复杂度优化到 $O(\log V)$。 -### 4.1 SPFA 算法的算法思想 +堆优化的主要思想是: +1. 使用优先队列存储当前已知的最短距离 +2. 每次从队列中取出距离最小的节点进行处理 +3. 当发现更短的路径时,将新的距离加入队列 +4. 通过优先队列的特性,保证每次取出的都是当前最小的距离 -### 4.2 SPFA 算法的实现步骤 +### 3.2 堆优化的 Dijkstra 算法实现步骤 -### 4.3 SPFA 算法的实现代码 +1. 初始化距离数组,将源节点的距离设为 $0$,其他节点的距离设为无穷大。 +2. 创建一个优先队列,将源节点及其距离 $(0, source)$ 加入队列。 +3. 当队列不为空时: + - 取出队列中距离最小的节点 + - 如果该节点的距离大于已知的最短距离,则跳过 + - 否则,遍历该节点的所有相邻节点 + - 如果通过当前节点到达相邻节点的距离更短,则更新距离并将新的距离加入队列 +4. 重复步骤 3,直到队列为空 +5. 返回所有节点的最短距离 + +### 3.3 堆优化的 Dijkstra 算法实现代码 ```python +import heapq + +class Solution: + def dijkstra(self, graph, n, source): + # 初始化距离数组 + dist = [float('inf') for _ in range(n + 1)] + dist[source] = 0 + + # 创建优先队列,存储 (距离, 节点) 的元组 + priority_queue = [(0, source)] + + while priority_queue: + current_distance, current_node = heapq.heappop(priority_queue) + + # 如果当前距离大于已知的最短距离,跳过 + if current_distance > dist[current_node]: + continue + + # 遍历当前节点的所有相邻节点 + for neighbor, weight in graph[current_node].items(): + distance = current_distance + weight + if distance < dist[neighbor]: + dist[neighbor] = distance + heapq.heappush(priority_queue, (distance, neighbor)) + + return dist + +# 使用示例 +# 创建一个有向图,使用邻接表表示 +graph = { + 1: {2: 2, 3: 4}, + 2: {3: 1, 4: 7}, + 3: {4: 3}, + 4: {} +} +n = 4 # 图中节点数量 +source = 1 # 源节点 + +dist = Solution().dijkstra(graph, n, source) +print("从节点", source, "到其他节点的最短距离:") +for i in range(1, n + 1): + if dist[i] == float('inf'): + print(f"到节点 {i} 的距离:不可达") + else: + print(f"到节点 {i} 的距离:{dist[i]}") ``` +代码解释: + +1. `graph` 是一个字典,表示图的邻接表。例如,`graph[1] = {2: 3, 3: 4}` 表示从节点 1 到节点 2 的边权重为 3,到节点 3 的边权重为 4。 +2. `n` 是图中顶点的数量。 +3. `source` 是源节点的编号。 +4. `dist` 数组存储源点到各个节点的最短距离。 +5. `priority_queue` 是一个优先队列,用来选择当前距离源点最近的节点。队列中的元素是 (距离, 节点) 的元组。 +6. 主循环中,每次从队列中取出距离最小的节点。如果该节点的距离已经被更新过,跳过。 +7. 对于当前节点的每一个邻居,计算通过当前节点到达邻居的距离。如果这个距离比已知的更短,更新距离并将邻居加入队列。 +8. 最终,`dist` 数组中存储的就是源点到所有节点的最短距离。 + +### 3.4 堆优化的 Dijkstra 算法复杂度分析 + +- **时间复杂度**:$O((V + E) \log V)$ + - 每个节点最多被加入优先队列一次,每次操作的时间复杂度为 $O(\log V)$ + - 每条边最多被处理一次,每次处理的时间复杂度为 $O(\log V)$ + - 因此总时间复杂度为 $O((V + E) \log V)$ + +- **空间复杂度**:$O(V)$ + - 需要存储距离数组,大小为 $O(V)$。 + - 优先队列在最坏情况下可能存储所有节点,大小为 $O(V)$。 diff --git a/Contents/08.Graph/04.Graph-Shortest-Path/02.Graph-Single-Source-Shortest-Path-02.md b/Contents/08.Graph/04.Graph-Shortest-Path/02.Graph-Single-Source-Shortest-Path-02.md index e69de29b..c5c1bf4e 100644 --- a/Contents/08.Graph/04.Graph-Shortest-Path/02.Graph-Single-Source-Shortest-Path-02.md +++ b/Contents/08.Graph/04.Graph-Shortest-Path/02.Graph-Single-Source-Shortest-Path-02.md @@ -0,0 +1,161 @@ +## 1. Bellman-Ford 算法 + +### 1.1 Bellman-Ford 算法的算法思想 + +> **Bellman-Ford 算法**:一种用于计算单源最短路径的算法,可以处理图中存在负权边的情况,并且能够检测负权环。 + +Bellman-Ford 算法的核心思想是: +1. 对图中的所有边进行 $V-1$ 次松弛操作,其中 $V$ 是图中顶点的数量 +2. 每次松弛操作都会尝试通过当前边来缩短源点到目标顶点的距离 +3. 如果在 $V-1$ 次松弛后还能继续松弛,说明图中存在负权环 +4. 算法可以处理负权边,但不能处理负权环 + +### 1.2 Bellman-Ford 算法的实现步骤 + +1. 初始化距离数组,将源节点的距离设为 $0$,其他节点的距离设为无穷大 +2. 进行 $V-1$ 次迭代,每次迭代: + - 遍历图中的所有边 + - 对每条边进行松弛操作:如果通过当前边可以缩短源点到目标顶点的距离,则更新距离 +3. 进行第 $V$ 次迭代,检查是否还能继续松弛: + - 如果还能松弛,说明图中存在负权环 + - 如果不能松弛,说明已经找到最短路径 +4. 返回最短路径距离数组 + +### 1.3 Bellman-Ford 算法的实现代码 + +```python +class Solution: + def bellmanFord(self, graph, n, source): + # 初始化距离数组 + dist = [float('inf') for _ in range(n + 1)] + dist[source] = 0 + + # 进行 V-1 次迭代 + for i in range(n - 1): + # 遍历所有边 + for vi in graph: + for vj in graph[vi]: + # 松弛操作 + if dist[vj] > graph[vi][vj] + dist[vi]: + dist[vj] = graph[vi][vj] + dist[vi] + + # 检查是否存在负权环 + for vi in graph: + for vj in graph[vi]: + if dist[vj] > dist[vi] + graph[vi][vj]: + return None # 存在负权环 + + return dist +``` + +代码解释: + +1. `graph` 是一个字典,表示图的邻接表。例如,`graph[1] = {2: 3, 3: 4}` 表示从节点 1 到节点 2 的边权重为 3,到节点 3 的边权重为 4。 +2. `n` 是图中顶点的数量。 +3. `source` 是源节点的编号。 +4. `dist` 数组存储源点到各个节点的最短距离。 +5. 外层循环进行 $V-1$ 次迭代,确保所有可能的最短路径都被找到。 +6. 内层循环遍历所有边,进行松弛操作。 +7. 最后检查是否存在负权环,如果存在则返回 None。 + +### 1.4 Bellman-Ford 算法复杂度分析 + +- **时间复杂度**:$O(VE)$ + - 需要进行 $V-1$ 次迭代 + - 每次迭代需要遍历所有边 $E$ + - 因此总时间复杂度为 $O(VE)$ + +- **空间复杂度**:$O(V)$ + - 需要存储距离数组,大小为 $O(V)$ + - 不需要额外的空间来存储图的结构,因为使用邻接表表示 + + +## 2. SPFA 算法 + +### 2.1 SPFA 算法的算法思想 + +> **SPFA 算法(Shortest Path Faster Algorithm)**:是 Bellman-Ford 算法的一种队列优化版本,通过使用队列来维护待更新的节点,从而减少不必要的松弛操作。 + +SPFA 算法的核心思想是: +1. 使用队列来维护待更新的节点,而不是像 Bellman-Ford 算法那样遍历所有边 +2. 只有当某个节点的距离被更新时,才将其加入队列 +3. 通过这种方式,避免了大量不必要的松弛操作,提高了算法的效率 +4. 算法可以处理负权边,并且能够检测负权环 + +### 2.2 SPFA 算法的实现步骤 + +1. 初始化距离数组,将源节点的距离设为 $0$,其他节点的距离设为无穷大 +2. 创建一个队列,将源节点加入队列 +3. 当队列不为空时: + - 取出队首节点 + - 遍历该节点的所有相邻节点 + - 如果通过当前节点可以缩短到相邻节点的距离,则更新距离 + - 如果相邻节点不在队列中,则将其加入队列 +4. 重复步骤 3,直到队列为空 +5. 返回最短路径距离数组 + +### 2.3 SPFA 算法的实现代码 + +```python +from collections import deque + +def spfa(graph, n, source): + # 初始化距离数组 + dist = [float('inf') for _ in range(n + 1)] + dist[source] = 0 + + # 初始化队列和访问数组 + queue = deque([source]) + in_queue = [False] * (n + 1) + in_queue[source] = True + + # 记录每个节点入队次数,用于检测负环 + count = [0] * (n + 1) + + while queue: + # 取出队首节点 + current = queue.popleft() + in_queue[current] = False + + # 遍历当前节点的所有相邻节点 + for neighbor, weight in graph[current].items(): + # 如果通过当前节点可以缩短距离 + if dist[neighbor] > dist[current] + weight: + dist[neighbor] = dist[current] + weight + count[neighbor] += 1 + + # 如果节点入队次数超过 n-1 次,说明存在负环 + if count[neighbor] >= n: + return None + + # 如果相邻节点不在队列中,将其加入队列 + if not in_queue[neighbor]: + queue.append(neighbor) + in_queue[neighbor] = True + + return dist +``` + +代码解释: + +1. `graph` 是一个字典,表示图的邻接表。例如,`graph[1] = {2: 3, 3: 4}` 表示从节点 1 到节点 2 的边权重为 3,到节点 3 的边权重为 4。 +2. `n` 是图中顶点的数量。 +3. `source` 是源节点的编号。 +4. `dist` 数组存储源点到各个节点的最短距离。 +5. `queue` 是一个双端队列,用于维护待更新的节点。 +6. `in_queue` 数组用于记录节点是否在队列中,避免重复入队。 +7. `count` 数组用于记录每个节点的入队次数,用于检测负环。 +8. 主循环中,每次从队列中取出一个节点,遍历其所有相邻节点,如果发现更短的路径则更新距离并将相邻节点加入队列。 +9. 如果某个节点的入队次数超过 $n-1$ 次,说明图中存在负环,返回 None。 + +### 2.4 SPFA 算法复杂度分析 + +- **时间复杂度**: + - 平均情况下:$O(kE)$,其中 $k$ 是每个节点的平均入队次数 + - 最坏情况下:$O(VE)$,与 Bellman-Ford 算法相同 + - 实际运行中,SPFA 算法通常比 Bellman-Ford 算法快很多 + +- **空间复杂度**:$O(V)$ + - 需要存储距离数组,大小为 $O(V)$ + - 需要存储队列和访问数组,大小为 $O(V)$ + - 因此总空间复杂度为 $O(V)$