From 58c6ab9591136e8e258e013924f5e668cf0b12e1 Mon Sep 17 00:00:00 2001 From: "dongsheng.zhao" <1245384330@qq.com> Date: Mon, 27 Jan 2025 07:16:54 +0800 Subject: [PATCH 01/12] bugfix: fix the mistakes in the description of insertion sort. --- .../01.Array/02.Array-Sort/03.Array-Insertion-Sort.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Contents/01.Array/02.Array-Sort/03.Array-Insertion-Sort.md b/Contents/01.Array/02.Array-Sort/03.Array-Insertion-Sort.md index 1506e567..670dcf67 100644 --- a/Contents/01.Array/02.Array-Sort/03.Array-Insertion-Sort.md +++ b/Contents/01.Array/02.Array-Sort/03.Array-Insertion-Sort.md @@ -14,13 +14,13 @@ 1. 初始状态下,有序区间为 $[0, 0]$,无序区间为 $[1, n - 1]$。 2. 第 $1$ 趟插入: 1. 取出无序区间 $[1, n - 1]$ 中的第 $1$ 个元素,即 $nums[1]$。 - 2. 从右到左遍历有序区间中的元素,将比 $nums[1]$ 小的元素向后移动 $1$ 位。 - 3. 如果遇到大于或等于 $nums[1]$ 的元素时,说明找到了插入位置,将 $nums[1]$ 插入到该位置。 + 2. 从右到左遍历有序区间中的元素,将比 $nums[1]$ 大的元素向后移动 $1$ 位。 + 3. 如果遇到小于或等于 $nums[1]$ 的元素时,说明找到了插入位置,将 $nums[1]$ 插入到该位置。 4. 插入元素后有序区间变为 $[0, 1]$,无序区间变为 $[2, n - 1]$。 3. 第 $2$ 趟插入: 1. 取出无序区间 $[2, n - 1]$ 中的第 $1$ 个元素,即 $nums[2]$。 - 2. 从右到左遍历有序区间中的元素,将比 $nums[2]$ 小的元素向后移动 $1$ 位。 - 3. 如果遇到大于或等于 $nums[2]$ 的元素时,说明找到了插入位置,将 $nums[2]$ 插入到该位置。 + 2. 从右到左遍历有序区间中的元素,将比 $nums[2]$ 大的元素向后移动 $1$ 位。 + 3. 如果遇到小于或等于 $nums[2]$ 的元素时,说明找到了插入位置,将 $nums[2]$ 插入到该位置。 4. 插入元素后有序区间变为 $[0, 2]$,无序区间变为 $[3, n - 1]$。 4. 依次类推,对剩余无序区间中的元素重复上述插入过程,直到所有元素都插入到有序区间中,排序结束。 From f8ee47376a8a8aea6d338dffa868fa0522ea7e76 Mon Sep 17 00:00:00 2001 From: "dongsheng.zhao" <1245384330@qq.com> Date: Thu, 6 Feb 2025 09:52:32 +0800 Subject: [PATCH 02/12] fix typo in Binary-Tree-Basic.md --- Contents/07.Tree/01.Binary-Tree/01.Binary-Tree-Basic.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Contents/07.Tree/01.Binary-Tree/01.Binary-Tree-Basic.md b/Contents/07.Tree/01.Binary-Tree/01.Binary-Tree-Basic.md index 982c2f06..25235fc8 100644 --- a/Contents/07.Tree/01.Binary-Tree/01.Binary-Tree-Basic.md +++ b/Contents/07.Tree/01.Binary-Tree/01.Binary-Tree-Basic.md @@ -11,7 +11,7 @@ 「树」具有以下的特点: - 有且仅有一个节点没有前驱节点,该节点被称为树的 **「根节点(Root)」** 。 -- 除了根节点以之,每个节点有且仅有一个直接前驱节点。 +- 除了根节点以外,每个节点有且仅有一个直接前驱节点。 - 包括根节点在内,每个节点可以有多个后继节点。 - 当 $n > 1$ 时,除了根节点之外的其他节点,可分为 $m(m > 0)$ 个互不相交的有限集合 $T_1, T_2, ..., T_m$,其中每一个集合本身又是一棵树,并且被称为根的 **「子树(SubTree)」**。 @@ -118,7 +118,7 @@ - 叶子节点只能出现在最下面两层。 - 最下层的叶子节点一定集中在该层最左边的位置上。 - 倒数第二层如果有叶子节点,则该层的叶子节点一定集中在右边的位置上。 -- 如果节点的度为 $1$,则该节点只偶遇左孩子节点,即不存在只有右子树的情况。 +- 如果节点的度为 $1$,则该节点只有左孩子节点,即不存在只有右孩子节点的情况。 - 同等节点数的二叉树中,完全二叉树的深度最小。 完全二叉树也可以使用类似满二叉树的节点编号的方式来定义。即从根节点编号为 $1$ 开始,按照层次从上至下,每一层从左至右进行编号。对于深度为 $i$ 且有 $n$ 个节点的二叉树,当且仅当每一个节点都与深度为 $k$ 的满二叉树中编号从 $1$ 至 $n$ 的节点意义对应时,该二叉树为完全二叉树。 From 67eacd62c63070446102463fe4048aaa9e4558ef Mon Sep 17 00:00:00 2001 From: jiangyuan <469391363@qq.com> Date: Thu, 22 May 2025 21:13:07 +0800 Subject: [PATCH 03/12] typo typo --- .../02.String-Single-Pattern-Matching/01.String-Brute-Force.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Contents/06.String/02.String-Single-Pattern-Matching/01.String-Brute-Force.md b/Contents/06.String/02.String-Single-Pattern-Matching/01.String-Brute-Force.md index bdabf56f..6f94d3fe 100644 --- a/Contents/06.String/02.String-Single-Pattern-Matching/01.String-Brute-Force.md +++ b/Contents/06.String/02.String-Single-Pattern-Matching/01.String-Brute-Force.md @@ -43,7 +43,7 @@ BF 算法非常简单,容易理解,但其效率很低。主要是因为在 在最理想的情况下(第一次匹配直接匹配成功),BF 算法的最佳时间复杂度是 $O(m)$。 -在一般情况下,根据等概率原则,平均搜索次数为 $\frac{(n + m)}{2}$,所以 Brute Force 算法的平均时间复杂度为 $O(n + m)$。 +在一般情况下,根据等概率原则,平均搜索次数为 $\frac{(n + m)}{2}$,所以 Brute Force 算法的平均时间复杂度为 $O(n × m)$。 ## 参考资料 From 1d7425b7fab9303d6b6233e88539c2ae63bcd95c Mon Sep 17 00:00:00 2001 From: jiangyuan <469391363@qq.com> Date: Thu, 22 May 2025 21:15:43 +0800 Subject: [PATCH 04/12] typo --- .../02.String-Single-Pattern-Matching/01.String-Brute-Force.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Contents/06.String/02.String-Single-Pattern-Matching/01.String-Brute-Force.md b/Contents/06.String/02.String-Single-Pattern-Matching/01.String-Brute-Force.md index 6f94d3fe..384d407b 100644 --- a/Contents/06.String/02.String-Single-Pattern-Matching/01.String-Brute-Force.md +++ b/Contents/06.String/02.String-Single-Pattern-Matching/01.String-Brute-Force.md @@ -43,7 +43,7 @@ BF 算法非常简单,容易理解,但其效率很低。主要是因为在 在最理想的情况下(第一次匹配直接匹配成功),BF 算法的最佳时间复杂度是 $O(m)$。 -在一般情况下,根据等概率原则,平均搜索次数为 $\frac{(n + m)}{2}$,所以 Brute Force 算法的平均时间复杂度为 $O(n × m)$。 +在一般情况下,根据等概率原则,平均搜索次数为 $\frac{(n + m)}{2}$,所以 Brute Force 算法的平均时间复杂度为 $O(n \times m)$。 ## 参考资料 From 9615377632581e0ef5b22956ae2b7e0a81776203 Mon Sep 17 00:00:00 2001 From: ITCharge Date: Wed, 28 May 2025 14:48:39 +0800 Subject: [PATCH 05/12] =?UTF-8?q?=E8=A1=A5=E5=85=85=E3=80=8C=E5=9B=BE?= =?UTF-8?q?=E7=9A=84=E6=9C=80=E5=B0=8F=E7=94=9F=E6=88=90=E6=A0=91=E3=80=8D?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../01.Graph-Minimum-Spanning-Tree.md | 144 +++++++++++++++++- 1 file changed, 137 insertions(+), 7 deletions(-) diff --git a/Contents/08.Graph/03.Graph-Spanning-Tree/01.Graph-Minimum-Spanning-Tree.md b/Contents/08.Graph/03.Graph-Spanning-Tree/01.Graph-Minimum-Spanning-Tree.md index d81c9a3d..18a83e2c 100644 --- a/Contents/08.Graph/03.Graph-Spanning-Tree/01.Graph-Minimum-Spanning-Tree.md +++ b/Contents/08.Graph/03.Graph-Spanning-Tree/01.Graph-Minimum-Spanning-Tree.md @@ -2,7 +2,7 @@ 在了解「最小生成树」之前,我们需要要先理解 「生成树」的概念。 -> **图的生成树(Spanning Tree)**:如果无向连通图 G 的一个子图是一棵包含图 G 所有顶点的树,则称该子图为 G 的生成树。生成树是连通图的包含图中的所有顶点的极小连通子图。图的生成树不惟一。从不同的顶点出发进行遍历,可以得到不同的生成树。 +> **生成树(Spanning Tree)**:如果无向连通图 G 的一个子图是一棵包含图 G 所有顶点的树,则称该子图为 G 的生成树。生成树是连通图的包含图中的所有顶点的极小连通子图。图的生成树不惟一。从不同的顶点出发进行遍历,可以得到不同的生成树。 换句话说,生成树是原图 G 的一个子图,它包含了原图 G 的所有顶点,并且通过选择图中一部分边连接这些顶点,使得子图中没有环。 @@ -51,12 +51,73 @@ ### 2.3 Prim 算法的实现代码 ```python - +class Solution: + # graph 为图的邻接矩阵,start 为起始顶点 + def Prim(self, graph, start): + size = len(graph) + vis = set() + dist = [float('inf') for _ in range(size)] + + ans = 0 # 最小生成树的边权和 + dist[start] = 0 # 初始化起始顶点到起始顶点的边权值为 0 + + for i in range(1, size): # 初始化起始顶点到其他顶点的边权值 + dist[i] = graph[start][i] + vis.add(start) # 将 start 顶点标记为已访问 + + for _ in range(size - 1): + min_dis = float('inf') + min_dis_pos = -1 + for i in range(size): + if i not in vis and dist[i] < min_dis: + min_dis = dist[i] + min_dis_pos = i + if min_dis_pos == -1: # 没有顶点可以加入 MST,图 G 不连通 + return -1 + ans += min_dis # 将顶点加入 MST,并将边权值加入到答案中 + vis.add(min_dis_pos) + for i in range(size): + if i not in vis and dist[i] > graph[min_dis_pos][i]: + dist[i] = graph[min_dis_pos][i] + return ans + +points = [[0,0]] +graph = dict() +size = len(points) +for i in range(size): + x1, y1 = points[i] + for j in range(size): + x2, y2 = points[j] + dist = abs(x2 - x1) + abs(y2 - y1) + if i not in graph: + graph[i] = dict() + if j not in graph: + graph[j] = dict() + graph[i][j] = dist + graph[j][i] = dist + + +print(Solution().Prim(graph)) ``` -### 2.3 Prim 算法 +### 2.4 Prim 算法复杂度分析 + +Prim 算法的时间复杂度主要取决于以下几个因素: + +1. **初始化阶段**: + - 初始化距离数组和访问数组的时间复杂度为 $O(V)$,其中 $V$ 是图中的顶点数。 -## 03. Kruskal 算法 +2. **主循环阶段**: + - 外层循环需要执行 $V-1$ 次,用于选择 $V-1$ 条边。 + - 每次循环中需要: + - 找到未访问顶点中距离最小的顶点,时间复杂度为 $O(V)$。 + - 更新相邻顶点的距离,时间复杂度为 $O(V)$。 + +因此,Prim 算法的总体复杂度为: +- 时间复杂度:$O(V^2)$,其中 $V$ 是图中的顶点数。 +- 空间复杂度:$O(V)$,主要用于存储距离数组和访问数组。 + +## 3. Kruskal 算法 ### 3.1 Kruskal 算法的算法思想 @@ -70,12 +131,81 @@ 2. 将每个顶点看做是一个单独集合,即初始时每个顶点自成一个集合。 3. 按照排好序的边顺序,按照权重从小到大,依次遍历每一条边。 4. 对于每条边,检查其连接的两个顶点所属的集合: - 1. 如果两个顶点属于同一个集合,则跳过这条边,以免形成环路。 - 2. 如果两个顶点不属于同一个集合,则将这条边加入到最小生成树中,同时合并这两个顶点所属的集合。 + 1. 如果两个顶点属于同一个集合,则跳过这条边,以免形成环路。 + 2. 如果两个顶点不属于同一个集合,则将这条边加入到最小生成树中,同时合并这两个顶点所属的集合。 5. 重复第 $3 \sim 4$ 步,直到最小生成树中的变数等于所有节点数减 $1$ 为止。 ### 3.3 Kruskal 算法的实现代码 ```python +class UnionFind: + + def __init__(self, n): + self.parent = [i for i in range(n)] + self.count = n + + def find(self, x): + while x != self.parent[x]: + self.parent[x] = self.parent[self.parent[x]] + x = self.parent[x] + return x + + def union(self, x, y): + root_x = self.find(x) + root_y = self.find(y) + if root_x == root_y: + return + + self.parent[root_x] = root_y + self.count -= 1 + + def is_connected(self, x, y): + return self.find(x) == self.find(y) + + +class Solution: + def Kruskal(self, edges, size): + union_find = UnionFind(size) + + edges.sort(key=lambda x: x[2]) + + res, cnt = 0, 1 + for x, y, dist in edges: + if union_find.is_connected(x, y): + continue + ans += dist + cnt += 1 + union_find.union(x, y) + if cnt == size - 1: + return ans + return ans + + def minCostConnectPoints(self, points: List[List[int]]) -> int: + size = len(points) + edges = [] + for i in range(size): + xi, yi = points[i] + for j in range(i + 1, size): + xj, yj = points[j] + dist = abs(xi - xj) + abs(yi - yj) + edges.append([i, j, dist]) + + ans = Solution().Kruskal(edges, size) + return ans +``` + +### 3.4 Kruskal 算法复杂度分析 + +Kruskal 算法的时间复杂度主要取决于以下几个因素: + +1. **边的排序**:对 $E$ 条边进行排序的时间复杂度为 $O(E \log E)$。 + +2. **并查集操作**: + - 查找操作(find)的时间复杂度为 $O(\alpha(n))$,其中 $\alpha(n)$ 是阿克曼函数的反函数,增长极其缓慢,可以近似认为是常数时间。 + - 合并操作(union)的时间复杂度也是 $O(\alpha(n))$。 + +3. **遍历边的过程**:需要遍历所有边,时间复杂度为 $O(E)$。 -``` \ No newline at end of file +因此,Kruskal 算法的总体时间复杂度为: +- 时间复杂度:$O(E \log E)$,其中 $E$ 是图中的边数。 +- 空间复杂度:$O(V)$,其中 $V$ 是图中的顶点数,主要用于存储并查集数据结构。 \ No newline at end of file From 02cf520ad693c5ae38a064114e1761bc61b42d33 Mon Sep 17 00:00:00 2001 From: ITCharge Date: Fri, 30 May 2025 16:37:17 +0800 Subject: [PATCH 06/12] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E3=80=8C=E5=8D=95?= =?UTF-8?q?=E6=BA=90=E6=9C=80=E7=9F=AD=E8=B7=AF=E5=BE=84=E7=9F=A5=E8=AF=86?= =?UTF-8?q?=EF=BC=88=E4=B8=80=EF=BC=89=E3=80=8D=E7=9B=B8=E5=85=B3=E5=86=85?= =?UTF-8?q?=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...01.Graph-Single-Source-Shortest-Path-01.md | 175 ++++++++++++++++-- 1 file changed, 163 insertions(+), 12 deletions(-) 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)$。 From 0cc0e179dcf58a6090dc1ac4f46a4dcac285df95 Mon Sep 17 00:00:00 2001 From: ITCharge Date: Fri, 30 May 2025 16:37:31 +0800 Subject: [PATCH 07/12] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E3=80=8C=E5=8D=95?= =?UTF-8?q?=E6=BA=90=E6=9C=80=E7=9F=AD=E8=B7=AF=E5=BE=84=E7=9F=A5=E8=AF=86?= =?UTF-8?q?=EF=BC=88=E4=BA=8C=EF=BC=89=E3=80=8D=E7=9B=B8=E5=85=B3=E5=86=85?= =?UTF-8?q?=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...02.Graph-Single-Source-Shortest-Path-02.md | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) 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)$ From 8be59cc1ad1e0ab625f88871546ba31f1dfa2dd3 Mon Sep 17 00:00:00 2001 From: ITCharge Date: Tue, 10 Jun 2025 15:37:23 +0800 Subject: [PATCH 08/12] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E3=80=8C=E5=A4=9A?= =?UTF-8?q?=E6=BA=90=E6=9C=80=E7=9F=AD=E8=B7=AF=E5=BE=84=E3=80=8D=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../04.Graph-Multi-Source-Shortest-Path.md | 202 ++++++++++++++++++ 1 file changed, 202 insertions(+) diff --git a/Contents/08.Graph/04.Graph-Shortest-Path/04.Graph-Multi-Source-Shortest-Path.md b/Contents/08.Graph/04.Graph-Shortest-Path/04.Graph-Multi-Source-Shortest-Path.md index e69de29b..46ee8248 100644 --- a/Contents/08.Graph/04.Graph-Shortest-Path/04.Graph-Multi-Source-Shortest-Path.md +++ b/Contents/08.Graph/04.Graph-Shortest-Path/04.Graph-Multi-Source-Shortest-Path.md @@ -0,0 +1,202 @@ +## 1. 多源最短路径简介 + +> **多源最短路径(All-Pairs Shortest Paths)**:对于一个带权图 $G = (V, E)$,计算图中任意两个顶点之间的最短路径长度。 + +多源最短路径问题的核心是找到图中任意两个顶点之间的最短路径。这个问题在许多实际应用中都非常重要,比如: + +1. 网络路由中的路由表计算 +2. 地图导航系统中的距离矩阵计算 +3. 社交网络中的最短关系链分析 +4. 交通网络中的最优路径规划 + +常见的解决多源最短路径问题的算法包括: + +1. **Floyd-Warshall 算法**:一种动态规划算法,可以处理负权边,但不能处理负权环。 +2. **Johnson 算法**:结合了 Bellman-Ford 算法和 Dijkstra 算法,可以处理负权边,但不能处理负权环。 +3. **重复 Dijkstra 算法**:对每个顶点运行一次 Dijkstra 算法,适用于无负权边的图。 + +## 2. Floyd-Warshall 算法 + +### 2.1 Floyd-Warshall 算法的算法思想 + +> **Floyd-Warshall 算法**:一种动态规划算法,通过逐步考虑中间顶点来更新任意两点之间的最短路径。 + +Floyd-Warshall 算法的核心思想是: + +1. 对于图中的任意两个顶点 $i$ 和 $j$,考虑是否存在一个顶点 $k$,使得从 $i$ 到 $k$ 再到 $j$ 的路径比已知的从 $i$ 到 $j$ 的路径更短 +2. 如果存在这样的顶点 $k$,则更新从 $i$ 到 $j$ 的最短路径 +3. 通过考虑所有可能的中间顶点 $k$,最终得到任意两点之间的最短路径 + +### 2.2 Floyd-Warshall 算法的实现步骤 + +1. 初始化距离矩阵 $dist$,其中 $dist[i][j]$ 表示从顶点 $i$ 到顶点 $j$ 的最短路径长度 +2. 对于每对顶点 $(i, j)$,如果存在边 $(i, j)$,则 $dist[i][j]$ 设为边的权重,否则设为无穷大 +3. 对于每个顶点 $k$,作为中间顶点: + - 对于每对顶点 $(i, j)$,如果 $dist[i][k] + dist[k][j] < dist[i][j]$,则更新 $dist[i][j]$ +4. 重复步骤 3,直到考虑完所有可能的中间顶点 +5. 返回最终的距离矩阵 + +### 2.3 Floyd-Warshall 算法的实现代码 + +```python +def floyd_warshall(graph, n): + # 初始化距离矩阵 + dist = [[float('inf') for _ in range(n)] for _ in range(n)] + + # 设置直接相连的顶点之间的距离 + for i in range(n): + dist[i][i] = 0 + for j, weight in graph[i].items(): + dist[i][j] = weight + + # 考虑每个顶点作为中间顶点 + for k in range(n): + for i in range(n): + for j in range(n): + if dist[i][k] != float('inf') and dist[k][j] != float('inf'): + dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]) + + return dist +``` + +代码解释: + +1. `graph` 是一个字典,表示图的邻接表。例如,`graph[0] = {1: 3, 2: 4}` 表示从节点 0 到节点 1 的边权重为 3,到节点 2 的边权重为 4。 +2. `n` 是图中顶点的数量。 +3. `dist` 是一个二维数组,存储任意两点之间的最短路径长度。 +4. 首先初始化距离矩阵,将对角线元素设为 0,表示顶点到自身的距离为 0。 +5. 然后设置直接相连的顶点之间的距离。 +6. 主循环中,对于每个顶点 $k$,考虑它作为中间顶点时,是否能缩短其他顶点之间的距离。 +7. 最终返回的距离矩阵中,$dist[i][j]$ 表示从顶点 $i$ 到顶点 $j$ 的最短路径长度。 + +### 2.4 Floyd-Warshall 算法复杂度分析 + +- **时间复杂度**:$O(V^3)$ + - 需要三层嵌套循环,分别遍历所有顶点 + - 因此总时间复杂度为 $O(V^3)$ + +- **空间复杂度**:$O(V^2)$ + - 需要存储距离矩阵,大小为 $O(V^2)$ + - 不需要额外的空间来存储图的结构,因为使用邻接表表示 + +Floyd-Warshall 算法的主要优势在于: + +1. 实现简单,容易理解 +2. 可以处理负权边 +3. 可以检测负权环(如果某个顶点到自身的距离变为负数,说明存在负权环) +4. 适用于稠密图 + +主要缺点: + +1. 时间复杂度较高,不适用于大规模图 +2. 空间复杂度较高,需要存储完整的距离矩阵 +3. 不能处理负权环 + +## 3. Johnson 算法 + +### 3.1 Johnson 算法的算法思想 + +> **Johnson 算法**:一种结合了 Bellman-Ford 算法和 Dijkstra 算法的多源最短路径算法,可以处理负权边,但不能处理负权环。 + +Johnson 算法的核心思想是: + +1. 通过重新赋权,将图中的负权边转换为非负权边 +2. 对每个顶点运行一次 Dijkstra 算法,计算最短路径 +3. 将结果转换回原始权重 + +### 3.2 Johnson 算法的实现步骤 + +1. 添加一个新的顶点 $s$,并添加从 $s$ 到所有其他顶点的边,权重为 0 +2. 使用 Bellman-Ford 算法计算从 $s$ 到所有顶点的最短路径 $h(v)$ +3. 重新赋权:对于每条边 $(u, v)$,新的权重为 $w(u, v) + h(u) - h(v)$ +4. 对每个顶点 $v$,使用 Dijkstra 算法计算从 $v$ 到所有其他顶点的最短路径 +5. 将结果转换回原始权重:对于从 $u$ 到 $v$ 的最短路径,原始权重为 $d(u, v) - h(u) + h(v)$ + +### 3.3 Johnson 算法的实现代码 + +```python +from collections import defaultdict +import heapq + +def johnson(graph, n): + # 添加新顶点 s + new_graph = defaultdict(dict) + for u in graph: + for v, w in graph[u].items(): + new_graph[u][v] = w + new_graph[n][u] = 0 # 从 s 到所有顶点的边权重为 0 + + # 使用 Bellman-Ford 算法计算 h(v) + h = [float('inf')] * (n + 1) + h[n] = 0 + + for _ in range(n): + for u in new_graph: + for v, w in new_graph[u].items(): + if h[v] > h[u] + w: + h[v] = h[u] + w + + # 检查是否存在负权环 + for u in new_graph: + for v, w in new_graph[u].items(): + if h[v] > h[u] + w: + return None # 存在负权环 + + # 重新赋权 + reweighted_graph = defaultdict(dict) + for u in graph: + for v, w in graph[u].items(): + reweighted_graph[u][v] = w + h[u] - h[v] + + # 对每个顶点运行 Dijkstra 算法 + dist = [[float('inf') for _ in range(n)] for _ in range(n)] + for source in range(n): + # 初始化距离数组 + d = [float('inf')] * n + d[source] = 0 + + # 使用优先队列 + pq = [(0, source)] + visited = set() + + while pq: + current_dist, u = heapq.heappop(pq) + if u in visited: + continue + visited.add(u) + + for v, w in reweighted_graph[u].items(): + if d[v] > current_dist + w: + d[v] = current_dist + w + heapq.heappush(pq, (d[v], v)) + + # 转换回原始权重 + for v in range(n): + if d[v] != float('inf'): + dist[source][v] = d[v] - h[source] + h[v] + + return dist +``` + +代码解释: + +1. `graph` 是一个字典,表示图的邻接表。 +2. `n` 是图中顶点的数量。 +3. 首先添加一个新的顶点 $s$,并添加从 $s$ 到所有其他顶点的边,权重为 0。 +4. 使用 Bellman-Ford 算法计算从 $s$ 到所有顶点的最短路径 $h(v)$。 +5. 检查是否存在负权环,如果存在则返回 None。 +6. 重新赋权,将图中的负权边转换为非负权边。 +7. 对每个顶点运行一次 Dijkstra 算法,计算最短路径。 +8. 将结果转换回原始权重,得到最终的距离矩阵。 + +### 3.4 Johnson 算法复杂度分析 + +- **时间复杂度**:$O(VE \log V)$ + - 需要运行一次 Bellman-Ford 算法,时间复杂度为 $O(VE)$ + - 需要运行 $V$ 次 Dijkstra 算法,每次时间复杂度为 $O(E \log V)$ + - 因此总时间复杂度为 $O(VE \log V)$ + +- **空间复杂度**:$O(V^2)$ + - 需要存储距离矩阵,大小为 $O(V^2)$ + - 需要存储重新赋权后的图,大小为 $O(E)$ + - 因此总空间复杂度为 $O(V^2)$ From 7eedd87450445f7d041ce891d0ad6f9336bf80ed Mon Sep 17 00:00:00 2001 From: ITCharge Date: Tue, 10 Jun 2025 15:38:08 +0800 Subject: [PATCH 09/12] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E3=80=8C=E6=AC=A1?= =?UTF-8?q?=E7=9F=AD=E8=B7=AF=E5=BE=84=E3=80=8D=E7=9B=B8=E5=85=B3=E5=86=85?= =?UTF-8?q?=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../06.Graph-The-Second-Shortest-Path.md | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/Contents/08.Graph/04.Graph-Shortest-Path/06.Graph-The-Second-Shortest-Path.md b/Contents/08.Graph/04.Graph-Shortest-Path/06.Graph-The-Second-Shortest-Path.md index e69de29b..d2abf0cb 100644 --- a/Contents/08.Graph/04.Graph-Shortest-Path/06.Graph-The-Second-Shortest-Path.md +++ b/Contents/08.Graph/04.Graph-Shortest-Path/06.Graph-The-Second-Shortest-Path.md @@ -0,0 +1,131 @@ +## 1.1 次短路径简介 + +> **次短路径**:给定一个带权有向图,求从起点到终点的次短路径。次短路径是指长度严格大于最短路径的所有路径中长度最小的那条路径。 + +### 1.1.1 问题特点 + +- 次短路径必须严格大于最短路径 +- 可能存在多条最短路径,但次短路径是唯一的 +- 如果不存在次短路径(如最短路径是唯一的),则返回 $-1$。 + +### 1.1.2 常见变体 + +1. 允许重复边的次短路径 +2. 不允许重复边的次短路径 +3. 带约束条件的次短路径(如必须经过某些节点) + +## 1.2 次短路径基本思路 + +求解次短路径的常用方法是使用 Dijkstra 算法的变体。基本思路如下: + +1. 使用 Dijkstra 算法找到最短路径。 +2. 在寻找最短路径的过程中,同时维护次短路径。 +3. 对于每个节点,我们需要维护两个距离值: + - $dist1[u]$:从起点到节点 u 的最短距离。 + - $dist2[u]$:从起点到节点 u 的次短距离。 + +### 1.2.1 具体实现步骤 + +1. 初始化 $dist1$ 和 $dist2$ 数组,所有值设为无穷大。 +2. 将起点加入优先队列,距离为 $0$。 +3. 每次从队列中取出距离最小的节点 $u$。 +4. 遍历 $u$ 的所有邻接节点 $v$: + - 如果找到更短的路径,更新 $dist1[v]$。 + - 如果找到次短的路径,更新 $dist2[v]$。 +5. 最终 $dist2[终点]$ 即为所求的次短路径长度。 + +### 1.2.2 算法正确性证明 + +1. 对于任意节点 $u$,$dist1[u]$ 一定是最短路径长度。 +2. 对于任意节点 $u$,$dist2[u]$ 一定是次短路径长度。 +3. 算法会考虑所有可能的路径,因此不会遗漏次短路径。 + +## 1.3 次短路径代码实现 + +```python +import heapq + +def second_shortest_path(n: int, edges: List[List[int]], start: int, end: int) -> int: + """ + 计算从起点到终点的次短路径长度 + + 参数: + n: 节点数量 + edges: 边列表,每个元素为 [起点, 终点, 权重] + start: 起始节点 + end: 目标节点 + + 返回: + 次短路径的长度,如果不存在则返回 -1 + """ + # 构建邻接表 + graph = [[] for _ in range(n)] + for u, v, w in edges: + graph[u].append((v, w)) + + # 初始化距离数组 + dist1 = [float('inf')] * n # 最短距离 + dist2 = [float('inf')] * n # 次短距离 + dist1[start] = 0 + + # 优先队列,存储 (距离, 节点) 的元组 + pq = [(0, start)] + + while pq: + d, u = heapq.heappop(pq) + + # 如果当前距离大于次短距离,跳过 + if d > dist2[u]: + continue + + # 遍历所有邻接节点 + for v, w in graph[u]: + # 计算新的距离 + new_dist = d + w + + # 如果找到更短的路径 + if new_dist < dist1[v]: + dist2[v] = dist1[v] # 原来的最短路径变成次短路径 + dist1[v] = new_dist # 更新最短路径 + heapq.heappush(pq, (new_dist, v)) + # 如果找到次短的路径 + elif new_dist > dist1[v] and new_dist < dist2[v]: + dist2[v] = new_dist + heapq.heappush(pq, (new_dist, v)) + + return dist2[end] if dist2[end] != float('inf') else -1 + +# 使用示例 +n = 4 +edges = [ + [0, 1, 1], + [1, 2, 2], + [2, 3, 1], + [0, 2, 4], + [1, 3, 5] +] +start = 0 +end = 3 + +result = second_shortest_path(n, edges, start, end) +print(f"次短路径长度: {result}") +``` + +## 1.4 算法复杂度分析 + +- 时间复杂度:$O((V + E)\log V)$,其中 $V$ 是节点数,$E$ 是边数。 +- 空间复杂度:$O(V)$,用于存储距离数组和优先队列。 + +## 1.5 应用场景 + +1. 网络路由:寻找备用路径。 +2. 交通规划:寻找替代路线。 +3. 通信网络:寻找备用通信路径。 +4. 物流配送:规划备用配送路线。 + +## 1.6 注意事项 + +1. 次短路径必须严格大于最短路径。 +2. 如果不存在次短路径,返回 $-1$。 +3. 图中可能存在负权边,此时需要使用 Bellman-Ford 算法的变体。 +4. 对于无向图,需要将每条边都加入两次。 \ No newline at end of file From 3b994eddea3952511045b4bcdd326e9cf0b1e112 Mon Sep 17 00:00:00 2001 From: ITCharge Date: Tue, 10 Jun 2025 15:38:23 +0800 Subject: [PATCH 10/12] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E3=80=8C=E5=B7=AE?= =?UTF-8?q?=E5=88=86=E7=BA=A6=E6=9D=9F=E7=B3=BB=E7=BB=9F=E3=80=8D=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ....Graph-System-Of-Difference-Constraints.md | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/Contents/08.Graph/04.Graph-Shortest-Path/08.Graph-System-Of-Difference-Constraints.md b/Contents/08.Graph/04.Graph-Shortest-Path/08.Graph-System-Of-Difference-Constraints.md index e69de29b..11f169fe 100644 --- a/Contents/08.Graph/04.Graph-Shortest-Path/08.Graph-System-Of-Difference-Constraints.md +++ b/Contents/08.Graph/04.Graph-Shortest-Path/08.Graph-System-Of-Difference-Constraints.md @@ -0,0 +1,120 @@ +## 1.1 差分约束系统简介 + +> **差分约束系统(System of Difference Constraints)**:一种特殊的线性规划问题,其中每个约束条件都是形如 $x_i - x_j \leq c$ 的不等式。这类问题可以通过图论中的最短路径算法来求解。 + +## 1.2 问题形式 + +给定一组形如 $x_i - x_j \leq c$ 的约束条件,其中: + +- $x_i, x_j$ 是变量。 +- $c$ 是常数。 + +我们的目标是找到一组满足所有约束条件的变量值。 + +## 1.3 图论建模 + +差分约束系统可以转化为有向图问题: + +1. 将每个变量 $x_i$ 看作图中的一个顶点。 +2. 对于约束 $x_i - x_j \leq c$,添加一条从 $j$ 到 $i$ 的边,权重为 $c$。 +3. 添加一个虚拟源点 $s$,向所有顶点连一条权重为 $0$ 的边。 + +## 1.4 求解方法 + +1. **Bellman-Ford 算法**: + - 如果图中存在负环,则无解。 + - 否则,从源点到各点的最短路径长度即为对应变量的解。 + +2. **SPFA 算法**: + - 队列优化的 Bellman-Ford 算法。 + - 适用于稀疏图。 + +## 1.5 应用场景 + +1. 任务调度问题 +2. 区间约束问题 +3. 资源分配问题 +4. 时间序列分析 + +## 1.6 代码实现 + +```python +def solve_difference_constraints(n, constraints): + # 构建图 + graph = [[] for _ in range(n + 1)] + for i, j, c in constraints: + graph[j].append((i, c)) + + # 添加虚拟源点 + for i in range(n): + graph[n].append((i, 0)) + + # Bellman-Ford 算法 + dist = [float('inf')] * (n + 1) + dist[n] = 0 + + # 松弛操作 + for _ in range(n): + for u in range(n + 1): + for v, w in graph[u]: + if dist[u] + w < dist[v]: + dist[v] = dist[u] + w + + # 检查负环 + for u in range(n + 1): + for v, w in graph[u]: + if dist[u] + w < dist[v]: + return None # 存在负环,无解 + + return dist[:n] # 返回前 n 个变量的解 +``` + +## 1.7 算法复杂度 + +- 时间复杂度: + + - **Bellman-Ford 算法**: + + - 最坏情况:$O(VE)$。 + + - 其中 $V$ 为顶点数,$E$ 为边数。 + + - 需要进行 $V-1$ 次松弛操作,每次操作遍历所有边。 + + - **SPFA 算法**: + - 平均情况:$O(kE)$,其中 $k$ 为每个点的平均入队次数。 + - 最坏情况:$O(VE)$。 + - 实际运行时间通常优于 Bellman-Ford 算法。 + +- 空间复杂度: + + - **Bellman-Ford 算法**: + + - $O(V + E)$ + + - 需要存储图结构:$O(V + E)$。 + + - 需要存储距离数组:$O(V)$。 + + - **SPFA 算法**: + + - $O(V + E)$。 + + - 需要存储图结构:$O(V + E)$。 + + - 需要存储距离数组:$O(V)$。 + + - 需要存储队列:$O(V)$。 + +### 1.8 优化建议 + +1. 对于稀疏图,优先使用 SPFA 算法。 +2. 对于稠密图,可以考虑使用 Bellman-Ford 算法。 +3. 如果问题规模较大,可以考虑使用其他优化算法或启发式方法。 + +### 1.9 注意事项 + +1. 差分约束系统可能有多个解 +2. 如果存在负环,则无解 +3. 实际应用中需要注意数值精度问题 +4. 对于大规模问题,可以考虑使用其他优化算法 \ No newline at end of file From 79c30ef5d8978e02c2d84f2853d02f6ee69b2262 Mon Sep 17 00:00:00 2001 From: ITCharge Date: Tue, 10 Jun 2025 15:39:32 +0800 Subject: [PATCH 11/12] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E3=80=8C=E4=BA=8C?= =?UTF-8?q?=E5=88=86=E5=9B=BE=E5=9F=BA=E7=A1=80=E7=9F=A5=E8=AF=86=E3=80=8D?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../01.Graph-Bipartite-Basic.md | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/Contents/08.Graph/05.Graph-Bipartite/01.Graph-Bipartite-Basic.md b/Contents/08.Graph/05.Graph-Bipartite/01.Graph-Bipartite-Basic.md index e69de29b..8007df93 100644 --- a/Contents/08.Graph/05.Graph-Bipartite/01.Graph-Bipartite-Basic.md +++ b/Contents/08.Graph/05.Graph-Bipartite/01.Graph-Bipartite-Basic.md @@ -0,0 +1,72 @@ +## 1.1 二分图的定义 + +> **二分图(Bipartite Graph)**:一种特殊的图,其顶点集可以被划分为两个互不相交的子集,使得图中的每一条边都连接着这两个子集中的顶点。换句话说,二分图中的顶点可以被分成两组,使得同一组内的顶点之间没有边相连。 + +## 1.2 二分图的性质 + +1. **染色性质**:二分图是二色的,即可以用两种颜色对顶点进行着色,使得相邻顶点颜色不同。 +2. **无奇环**:二分图中不存在长度为奇数的环。 +3. **最大匹配**:二分图的最大匹配问题可以通过匈牙利算法或网络流算法高效求解。 + +## 1.3 二分图的判定 + +判断一个图是否为二分图的方法: + +1. 使用深度优先搜索(DFS)或广度优先搜索(BFS)进行二着色 +2. 如果在染色过程中发现相邻顶点颜色相同,则该图不是二分图 +3. 如果能够成功完成二着色,则该图是二分图 + +## 1.4 二分图的应用场景 + +1. **任务分配**:将工人和任务分别作为两个顶点集,边表示工人可以完成的任务 +2. **婚姻匹配**:将男性和女性分别作为两个顶点集,边表示可能的配对关系 +3. **网络流问题**:许多网络流问题可以转化为二分图最大匹配问题 +4. **资源分配**:将资源和需求分别作为两个顶点集,边表示资源可以满足的需求 + +## 1.5 二分图的基本算法 + +1. **匈牙利算法**:用于求解二分图的最大匹配 +2. **Hopcroft-Karp算法**:用于求解二分图的最大匹配,时间复杂度更优 +3. **网络流算法**:将二分图最大匹配问题转化为最大流问题求解 + +## 1.6 二分图的判定代码 + +```python +def is_bipartite(graph): + """ + 判断图是否为二分图 + :param graph: 邻接表表示的图 + :return: 是否为二分图 + """ + n = len(graph) + colors = [0] * n # 0表示未染色,1和-1表示两种不同的颜色 + + def dfs(node, color): + colors[node] = color + for neighbor in graph[node]: + if colors[neighbor] == color: + return False + if colors[neighbor] == 0 and not dfs(neighbor, -color): + return False + return True + + for i in range(n): + if colors[i] == 0 and not dfs(i, 1): + return False + return True +``` + +## 1.7 常见问题类型 + +1. 判断图是否为二分图 +2. 求二分图的最大匹配 +3. 求二分图的最小点覆盖 +4. 求二分图的最大独立集 +5. 求二分图的最小路径覆盖 + +## 1.8 注意事项 + +1. 在实现二分图算法时,需要注意图的表示方式(邻接表或邻接矩阵) +2. 对于大规模图,需要考虑算法的空间复杂度 +3. 在实际应用中,可能需要根据具体问题对基本算法进行优化 +4. 处理有向图时,需要先将其转换为无向图再判断是否为二分图 \ No newline at end of file From 20e53edce15c8b1243a5f74c7b624b57a8a06222 Mon Sep 17 00:00:00 2001 From: ITCharge Date: Tue, 10 Jun 2025 15:40:02 +0800 Subject: [PATCH 12/12] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E3=80=8C=E4=BA=8C?= =?UTF-8?q?=E5=88=86=E5=9B=BE=E6=9C=80=E5=A4=A7=E5=8C=B9=E9=85=8D=E3=80=8D?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../03.Graph-Bipartite-Matching.md | 277 ++++++++++++++++++ 1 file changed, 277 insertions(+) diff --git a/Contents/08.Graph/05.Graph-Bipartite/03.Graph-Bipartite-Matching.md b/Contents/08.Graph/05.Graph-Bipartite/03.Graph-Bipartite-Matching.md index e69de29b..19cb4031 100644 --- a/Contents/08.Graph/05.Graph-Bipartite/03.Graph-Bipartite-Matching.md +++ b/Contents/08.Graph/05.Graph-Bipartite/03.Graph-Bipartite-Matching.md @@ -0,0 +1,277 @@ +## 1. 二分图最大匹配简介 + +> **二分图最大匹配(Maximum Bipartite Matching)**:图论中的一个重要问题。在二分图中,我们需要找到最大的匹配数,即最多可以有多少对顶点之间形成匹配。 + +- **二分图**:图中的顶点可以被分成两个独立的集合,使得每条边的两个端点分别属于这两个集合。 +- **匹配**:一组边的集合,其中任意两条边都没有共同的顶点。 +- **最大匹配**:包含边数最多的匹配。 + +### 1.1 应用场景 + +二分图最大匹配在实际应用中有广泛的应用: + +1. **任务分配**:将任务分配给工人,每个工人只能完成一个任务 +2. **婚姻匹配**:将男生和女生进行配对 +3. **网络流问题**:可以转化为最大流问题求解 +4. **资源分配**:将资源分配给需求方 +5. **学生选课**:将学生与课程进行匹配 +6. **网络路由**:将数据包与可用路径进行匹配 + +### 1.2 优化方法 + +1. **使用邻接表**:对于稀疏图,使用邻接表可以显著减少空间复杂度 +2. **双向搜索**:同时从左右两侧进行搜索,可以减少搜索次数 +3. **预处理**:对图进行预处理,去除不可能形成匹配的边 +4. **贪心匹配**:先进行贪心匹配,减少后续搜索的复杂度 +5. **并行处理**:对于大规模图,可以使用并行算法提高效率 + +## 2. 匈牙利算法 + +### 2.1 匈牙利算法基本思想 + +匈牙利算法(Hungarian Algorithm)是求解二分图最大匹配的经典算法。其基本思想是: + +1. 从左侧集合中任选一个未匹配的点开始 +2. 尝试寻找增广路径 +3. 如果找到增广路径,则更新匹配 +4. 重复以上步骤直到无法找到增广路径 + +### 2.2 匈牙利算法实现代码 + +```python +def max_bipartite_matching(graph, left_size, right_size): + # 初始化匹配数组 + match_right = [-1] * right_size + result = 0 + + # 对左侧每个顶点尝试匹配 + for left in range(left_size): + # 记录右侧顶点是否被访问过 + visited = [False] * right_size + + # 如果找到增广路径,则匹配数加1 + if find_augmenting_path(graph, left, visited, match_right): + result += 1 + + return result + +def find_augmenting_path(graph, left, visited, match_right): + # 遍历右侧所有顶点 + for right in range(len(graph[left])): + # 如果存在边且右侧顶点未被访问 + if graph[left][right] and not visited[right]: + visited[right] = True + + # 如果右侧顶点未匹配,或者可以找到新的匹配 + if match_right[right] == -1 or find_augmenting_path(graph, match_right[right], visited, match_right): + match_right[right] = left + return True + + return False +``` + +### 2.3 匈牙利算法时间复杂度 + +- 匈牙利算法的时间复杂度为 O(VE),其中 V 是顶点数,E 是边数 +- 使用邻接矩阵存储图时,空间复杂度为 O(V²) +- 使用邻接表存储图时,空间复杂度为 O(V + E) + + +## 3. Hopcroft-Karp 算法 + +### 3.1 Hopcroft-Karp 算法基本思想 + +Hopcroft-Karp 算法是求解二分图最大匹配的一个更高效的算法,时间复杂度为 O(√VE)。其基本思想是: + +1. 同时寻找多条不相交的增广路径 +2. 使用 BFS 分层,然后使用 DFS 寻找增广路径 +3. 每次迭代可以找到多条增广路径 + + +### 3.2 Hopcroft-Karp 算法实现代码 + +```python +from collections import deque + +def hopcroft_karp(graph, left_size, right_size): + # 初始化匹配数组 + match_left = [-1] * left_size + match_right = [-1] * right_size + result = 0 + + while True: + # 使用 BFS 寻找增广路径 + dist = [-1] * left_size + queue = deque() + + # 将未匹配的左侧顶点加入队列 + for i in range(left_size): + if match_left[i] == -1: + dist[i] = 0 + queue.append(i) + + # BFS 分层 + while queue: + left = queue.popleft() + for right in graph[left]: + if match_right[right] == -1: + # 找到增广路径 + break + if dist[match_right[right]] == -1: + dist[match_right[right]] = dist[left] + 1 + queue.append(match_right[right]) + + # 使用 DFS 寻找增广路径 + def dfs(left): + for right in graph[left]: + if match_right[right] == -1 or \ + (dist[match_right[right]] == dist[left] + 1 and \ + dfs(match_right[right])): + match_left[left] = right + match_right[right] = left + return True + return False + + # 尝试为每个未匹配的左侧顶点寻找增广路径 + found = False + for i in range(left_size): + if match_left[i] == -1 and dfs(i): + found = True + result += 1 + + if not found: + break + + return result +``` + +### 3.3 Hopcroft-Karp 算法复杂度 + +- **时间复杂度**:O(√VE),其中 V 是顶点数,E 是边数 +- **空间复杂度**:O(V + E) +- **优点**: + 1. 比匈牙利算法更高效 + 2. 适合处理大规模图 + 3. 可以并行化实现 +- **缺点**: + 1. 实现相对复杂 + 2. 常数因子较大 + 3. 对于小规模图可能不如匈牙利算法 + +### 3.4 Hopcroft-Karp 算法优化 + +1. **双向 BFS**:同时从左右两侧进行 BFS,减少搜索空间 +2. **动态分层**:根据当前匹配状态动态调整分层策略 +3. **预处理**:使用贪心算法进行初始匹配 +4. **并行化**:利用多线程或分布式计算提高效率 + +## 4. 网络流算法 + +### 4.1 网络流算法实现步骤 + +二分图最大匹配问题可以转化为最大流问题来求解。具体步骤如下: + +1. 添加源点和汇点 +2. 将二分图转化为网络流图 +3. 使用最大流算法求解 + +### 4.2 网络流算法实现代码 + +```python +from collections import defaultdict + +def max_flow_bipartite_matching(graph, left_size, right_size): + # 构建网络流图 + flow_graph = defaultdict(dict) + source = left_size + right_size + sink = source + 1 + + # 添加源点到左侧顶点的边 + for i in range(left_size): + flow_graph[source][i] = 1 + flow_graph[i][source] = 0 + + # 添加右侧顶点到汇点的边 + for i in range(right_size): + flow_graph[left_size + i][sink] = 1 + flow_graph[sink][left_size + i] = 0 + + # 添加二分图中的边 + for i in range(left_size): + for j in graph[i]: + flow_graph[i][left_size + j] = 1 + flow_graph[left_size + j][i] = 0 + + # 使用 Ford-Fulkerson 算法求解最大流 + def bfs(): + parent = [-1] * (sink + 1) + queue = deque([source]) + parent[source] = -2 + + while queue: + u = queue.popleft() + for v, capacity in flow_graph[u].items(): + if parent[v] == -1 and capacity > 0: + parent[v] = u + if v == sink: + return parent + queue.append(v) + return None + + def ford_fulkerson(): + max_flow = 0 + while True: + parent = bfs() + if not parent: + break + + # 找到增广路径上的最小容量 + v = sink + min_capacity = float('inf') + while v != source: + u = parent[v] + min_capacity = min(min_capacity, flow_graph[u][v]) + v = u + + # 更新流量 + v = sink + while v != source: + u = parent[v] + flow_graph[u][v] -= min_capacity + flow_graph[v][u] += min_capacity + v = u + + max_flow += min_capacity + + return max_flow + + return ford_fulkerson() +``` + +### 4.3 网络流算法复杂度 + +- **时间复杂度**: + 1. Ford-Fulkerson 算法:O(VE²) + 2. Dinic 算法:O(V²E) + 3. ISAP 算法:O(V²E) +- **空间复杂度**:O(V + E) + +## 5. 算法复杂度分析 + +1. **匈牙利算法** + - 时间复杂度:O(VE) + - 优点:实现简单,容易理解 + - 缺点:对于大规模图效率较低 + - 适用场景:小规模图,需要快速实现 + +2. **Hopcroft-Karp 算法** + - 时间复杂度:O(√VE) + - 优点:效率更高,适合大规模图 + - 缺点:实现相对复杂 + - 适用场景:大规模图,需要高效算法 + +3. **网络流算法** + - 时间复杂度:O(VE²) 或 O(V²E) + - 优点:可以处理更复杂的问题,如带权匹配 + - 缺点:实现复杂,常数较大 + - 适用场景:带权匹配,复杂约束条件