From ac4e899c5913e21bfbd661edad608bd0031e47d7 Mon Sep 17 00:00:00 2001 From: ITCharge Date: Mon, 22 Sep 2025 15:19:00 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20LaTex=20=E5=85=AC?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/01_array/01_15_array_two_pointers.md | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/docs/01_array/01_15_array_two_pointers.md b/docs/01_array/01_15_array_two_pointers.md index 8c4aafd3..748727bc 100644 --- a/docs/01_array/01_15_array_two_pointers.md +++ b/docs/01_array/01_15_array_two_pointers.md @@ -345,16 +345,10 @@ while left_1 < len(nums1) and left_2 < len(nums2): ##### 思路 1:分离双指针 1. 先对 $nums1$ 和 $nums2$ 排序。 -2. 用两个指针 $left\_1$、$left\underline{\hspace{0. -5em}}2$ 分别从两个数组头部开始遍历。 -3. 若 $nums1[left\_1] == nums2[left\underline -{\hspace{0.5em}}2]$,将该元素(去重)加入结果,并同时右移 $left\_1$、$left\underline -{\hspace{0.5em}}2$。 -4. 若 $nums1[left\_1] < nums2[left\underline -{\hspace{0.5em}}2]$,则 $left\_1$ 右移。 -5. 若 $nums1[left\_1] > nums2[left\underline -{\hspace{0.5em}}2]$,则 $left\underline -{\hspace{0.5em}}2$ 右移。 +2. 用两个指针 $left\_1$、$left\_2$ 分别从两个数组头部开始遍历。 +3. 若 $nums1[left\_1] == nums2[left\_2]$,将该元素(去重)加入结果,并同时右移 $left\_1$、$left\_2$。 +4. 若 $nums1[left\_1] < nums2[left\_2]$,则 $left\_1$ 右移。 +5. 若 $nums1[left\_1] > nums2[left\_2]$,则 $left\_2$ 右移。 6. 遍历结束后返回结果数组。 ##### 思路 1:代码 From e1552a9bfa22215ce72e90c675d65a318f6dae2a Mon Sep 17 00:00:00 2001 From: ITCharge Date: Mon, 22 Sep 2025 15:19:16 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=92=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E8=AF=AD=E5=8F=A5=E8=A1=A8=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/06_graph/06_07_graph_shortest_path_01.md | 79 +++++++++---------- 1 file changed, 37 insertions(+), 42 deletions(-) diff --git a/docs/06_graph/06_07_graph_shortest_path_01.md b/docs/06_graph/06_07_graph_shortest_path_01.md index 7581fe95..9ea277eb 100644 --- a/docs/06_graph/06_07_graph_shortest_path_01.md +++ b/docs/06_graph/06_07_graph_shortest_path_01.md @@ -128,54 +128,61 @@ for i in range(1, n + 1): ### 3.2 堆优化 Dijkstra 算法实现步骤 1. 初始化距离数组,源点距离设为 $0$,其余节点设为无穷大。 -2. 创建优先队列,将 $(0, source)$ 入队。 +2. 创建优先队列,将源节点及其距离 $(0, source)$ 入队。 3. 当优先队列非空时,重复以下操作: - 弹出队首(距离最小)节点; - - 若该节点的距离已大于当前最短距离,跳过; + - 如果该节点的距离已大于当前最短距离,跳过; - 否则,遍历其所有邻居,尝试松弛: - - 若通过当前节点到邻居的距离更短,则更新距离并将新距离入队。 + - 如果通过当前节点到邻居的距离更短,则更新距离并将新距离入队。 4. 队列为空时结束,返回所有节点的最短距离数组。 -### 3.3 堆优化的 Dijkstra 算法实现代码 +### 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 - - # 创建优先队列,存储 (距离, 节点) 的元组 + """ + 堆优化 Dijkstra 算法,计算单源最短路径 + :param graph: 邻接表,graph[u] = {v: w, ...} + :param n: 节点总数(节点编号从 1 到 n) + :param source: 源点编号 + :return: dist[i] 表示源点到 i 的最短距离 + """ + # 距离数组,初始化为无穷大 + dist = [float('inf')] * (n + 1) + dist[source] = 0 # 源点到自身距离为 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)) - + + # 遍历当前节点的所有邻居 + for neighbor, weight in graph.get(current_node, {}).items(): + new_distance = current_distance + weight + # 如果找到更短路径,则更新并入堆 + if new_distance < dist[neighbor]: + dist[neighbor] = new_distance + heapq.heappush(priority_queue, (new_distance, neighbor)) + return dist # 使用示例 -# 创建一个有向图,使用邻接表表示 +# 构建一个有向图,邻接表表示 graph = { 1: {2: 2, 3: 4}, 2: {3: 1, 4: 7}, 3: {4: 3}, 4: {} } -n = 4 # 图中节点数量 -source = 1 # 源节点 +n = 4 # 节点数量 +source = 1 # 源点编号 dist = Solution().dijkstra(graph, n, source) print("从节点", source, "到其他节点的最短距离:") @@ -186,27 +193,15 @@ for i in range(1, n + 1): 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 算法复杂度分析 +### 3.4 堆优化 Dijkstra 算法复杂度分析 -- **时间复杂度**:$O((V + E) \log V)$ - - 每个节点最多被加入优先队列一次,每次操作的时间复杂度为 $O(\log V)$ - - 每条边最多被处理一次,每次处理的时间复杂度为 $O(\log V)$ - - 因此总时间复杂度为 $O((V + E) \log V)$ +- **时间复杂度**:$O((V + E) \log V)$。 + - 堆优化 Dijkstra 算法中,每个节点最多会被弹出优先队列一次,每次弹出操作的复杂度为 $O(\log V)$。 + - 每条边在松弛操作时最多会导致一次入堆,入堆操作的复杂度同样为 $O(\log V)$。 + - 因此,总体时间复杂度为 $O((V + E) \log V)$,其中 $V$ 为节点数,$E$ 为边数。 -- **空间复杂度**:$O(V)$ - - 需要存储距离数组,大小为 $O(V)$。 - - 优先队列在最坏情况下可能存储所有节点,大小为 $O(V)$。 +- **空间复杂度**:$O(V)$。 + - 主要空间消耗在距离数组和优先队列,二者最坏情况下均为 $O(V)$ 级别。 ## 练习题目 From 2a4e239b26812de5045e84b3cfff3730580219b5 Mon Sep 17 00:00:00 2001 From: ITCharge Date: Mon, 22 Sep 2025 15:19:36 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=92=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E8=AF=AD=E5=8F=A5=E8=A1=A8=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../06_09_graph_multi_source_shortest_path.md | 221 +++++++++--------- 1 file changed, 108 insertions(+), 113 deletions(-) diff --git a/docs/06_graph/06_09_graph_multi_source_shortest_path.md b/docs/06_graph/06_09_graph_multi_source_shortest_path.md index c0c7db58..2acc9351 100644 --- a/docs/06_graph/06_09_graph_multi_source_shortest_path.md +++ b/docs/06_graph/06_09_graph_multi_source_shortest_path.md @@ -1,116 +1,119 @@ ## 1. 多源最短路径简介 -> **多源最短路径(All-Pairs Shortest Paths)**:对于一个带权图 $G = (V, E)$,计算图中任意两个顶点之间的最短路径长度。 +> **多源最短路径(All-Pairs Shortest Paths)**:指的是在一个带权图 $G = (V, E)$ 中,计算任意两个顶点之间的最短路径长度。 -多源最短路径问题的核心是找到图中任意两个顶点之间的最短路径。这个问题在许多实际应用中都非常重要,比如: +多源最短路径问题的本质,就是要找出图中每一对顶点之间的最短路径。这类问题在实际生活和工程中非常常见,例如: -1. 网络路由中的路由表计算 -2. 地图导航系统中的距离矩阵计算 -3. 社交网络中的最短关系链分析 -4. 交通网络中的最优路径规划 +1. 网络通信中,生成路由表以确定任意两点之间的最优传输路径; +2. 地图导航系统中,计算所有地点之间的距离矩阵; +3. 社交网络分析中,寻找两个人之间的最短关系链; +4. 交通网络中,规划任意两地之间的最优行车路线。 -常见的解决多源最短路径问题的算法包括: +常用的多源最短路径算法有: -1. **Floyd-Warshall 算法**:一种动态规划算法,可以处理负权边,但不能处理负权环。 -2. **Johnson 算法**:结合了 Bellman-Ford 算法和 Dijkstra 算法,可以处理负权边,但不能处理负权环。 -3. **重复 Dijkstra 算法**:对每个顶点运行一次 Dijkstra 算法,适用于无负权边的图。 +1. **Floyd-Warshall 算法**:一种基于动态规划的方法,能处理负权边,但无法处理负权环。 +2. **Johnson 算法**:结合了 Bellman-Ford 和 Dijkstra 算法,既能处理负权边,也能高效应对稀疏图,但同样不能处理负权环。 +3. **多次 Dijkstra 算法**:对每个顶点分别运行一次 Dijkstra 算法,适用于没有负权边的图。 ## 2. Floyd-Warshall 算法 -### 2.1 Floyd-Warshall 算法的算法思想 +### 2.1 Floyd-Warshall 算法的核心思想 -> **Floyd-Warshall 算法**:一种动态规划算法,通过逐步考虑中间顶点来更新任意两点之间的最短路径。 +> **Floyd-Warshall 算法**:这是一种经典的动态规划算法,通过不断尝试引入不同的中间节点,来优化任意两点之间的最短路径。 -Floyd-Warshall 算法的核心思想是: +通俗来说,Floyd-Warshall 算法的核心思想如下: -1. 对于图中的任意两个顶点 $i$ 和 $j$,考虑是否存在一个顶点 $k$,使得从 $i$ 到 $k$ 再到 $j$ 的路径比已知的从 $i$ 到 $j$ 的路径更短 -2. 如果存在这样的顶点 $k$,则更新从 $i$ 到 $j$ 的最短路径 -3. 通过考虑所有可能的中间顶点 $k$,最终得到任意两点之间的最短路径 +1. 假设要找从顶点 $i$ 到顶点 $j$ 的最短路径,试着经过某个中间顶点 $k$,看看能不能让路径更短。 +2. 如果发现「先从 $i$ 到 $k$,再从 $k$ 到 $j$」的路径比原来「直接从 $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. 返回最终的距离矩阵 +1. 先初始化一个距离矩阵 $dist$,$dist[i][j]$ 表示从顶点 $i$ 到顶点 $j$ 的当前最短路径长度。 +2. 如果 $i$ 和 $j$ 之间有直接的边,就把 $dist[i][j]$ 设为这条边的权重;如果没有,设为无穷大(表示不可达)。 +3. 然后,依次枚举每个顶点 $k$ 作为「中转站」: + - 对于所有顶点对 $(i, j)$,如果「从 $i$ 经过 $k$ 到 $j$」的路径更短(即 $dist[i][k] + dist[k][j] < dist[i][j]$),就用更短的路径更新 $dist[i][j]$。 +4. 重复第 3 步,直到所有顶点都被作为中间点尝试过。 +5. 最终,$dist$ 矩阵中每个 $dist[i][j]$ 就是从 $i$ 到 $j$ 的最短路径长度。 ### 2.3 Floyd-Warshall 算法的实现代码 ```python def floyd_warshall(graph, n): - # 初始化距离矩阵 - dist = [[float('inf') for _ in range(n)] for _ in range(n)] + """ + Floyd-Warshall 算法,计算所有点对之间的最短路径。 + :param graph: 邻接表,graph[i] = {j: weight, ...},节点编号为 0~n-1 + :param n: 节点总数 + :return: dist 矩阵,dist[i][j] 表示 i 到 j 的最短路径长度 + """ + # 初始化距离矩阵,所有点对距离设为无穷大 + dist = [[float('inf')] * n for _ in range(n)] - # 设置直接相连的顶点之间的距离 + # 距离矩阵对角线设为 0,表示自己到自己的距离为 0 for i in range(n): dist[i][i] = 0 - for j, weight in graph[i].items(): + # 设置直接相连的顶点之间的距离 + for j, weight in graph.get(i, {}).items(): dist[i][j] = weight - - # 考虑每个顶点作为中间顶点 + + # 三重循环,枚举每个中间点 k for k in range(n): for i in range(n): + # 跳过不可达的起点 + if dist[i][k] == float('inf'): + continue 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]) + # 跳过不可达的终点 + if dist[k][j] == float('inf'): + continue + # 如果经过 k 能让 i 到 j 更短,则更新 + if dist[i][j] > dist[i][k] + dist[k][j]: + dist[i][j] = dist[i][k] + dist[k][j] return dist ``` -代码解释: +### 2.4 Floyd-Warshall 算法分析 -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$ 的最短路径长度。 +- **时间复杂度**:$O(V^3)$ + - 算法包含三重嵌套循环,分别枚举所有中间点、起点和终点,因此总时间复杂度为 $O(V^3)$。 -### 2.4 Floyd-Warshall 算法复杂度分析 +- **空间复杂度**:$O(V^2)$ + - 主要空间消耗在距离矩阵 $dist$,需要 $O(V^2)$ 的空间。 + - 由于采用邻接表存储原图结构,无需额外空间存储图的边。 -- **时间复杂度**:$O(V^3)$ - - 需要三层嵌套循环,分别遍历所有顶点 - - 因此总时间复杂度为 $O(V^3)$ - -- **空间复杂度**:$O(V^2)$ - - 需要存储距离矩阵,大小为 $O(V^2)$ - - 不需要额外的空间来存储图的结构,因为使用邻接表表示 +**Floyd-Warshall 算法优点**: -Floyd-Warshall 算法的主要优势在于: +1. 实现简洁,易于理解和编码。 +2. 能处理负权边(但不能有负权环)。 +3. 可用于检测负权环(若某个顶点 $i$ 满足 $dist[i][i] < 0$,则存在负权环)。 +4. 特别适合稠密图(边数接近 $V^2$)。 -1. 实现简单,容易理解 -2. 可以处理负权边 -3. 可以检测负权环(如果某个顶点到自身的距离变为负数,说明存在负权环) -4. 适用于稠密图 +**Floyd-Warshall 算法缺点**: -主要缺点: - -1. 时间复杂度较高,不适用于大规模图 -2. 空间复杂度较高,需要存储完整的距离矩阵 -3. 不能处理负权环 +1. 时间复杂度较高,不适合节点数很大的图。 +2. 空间复杂度较高,需要维护完整的 $V \times V$ 距离矩阵。 +3. 无法处理存在负权环的情况(若有负权环,最短路无意义)。 ## 3. Johnson 算法 -### 3.1 Johnson 算法的算法思想 +### 3.1 Johnson 算法的核心思想 -> **Johnson 算法**:一种结合了 Bellman-Ford 算法和 Dijkstra 算法的多源最短路径算法,可以处理负权边,但不能处理负权环。 +> **Johnson 算法**:是一种结合 Bellman-Ford 和 Dijkstra 算法的多源最短路径算法,能够处理负权边,但无法处理负权环。 -Johnson 算法的核心思想是: +Johnson 算法的核心思想如下: -1. 通过重新赋权,将图中的负权边转换为非负权边 -2. 对每个顶点运行一次 Dijkstra 算法,计算最短路径 -3. 将结果转换回原始权重 +1. 通过对图进行重新赋权,将所有边权变为非负,从而使 Dijkstra 算法适用; +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)$ +1. 向原图添加一个新顶点 $s$,并从 $s$ 向所有其他顶点连一条权重为 0 的边; +2. 使用 Bellman-Ford 算法以 $s$ 为源点,计算 $s$ 到每个顶点 $v$ 的最短距离 $h(v)$; +3. 对于原图中的每条边 $(u, v)$,将其权重调整为 $w'(u, v) = w(u, v) + h(u) - h(v)$,使所有边权非负; +4. 对每个顶点 $u$,以 $u$ 为源点在重新赋权后的图上运行 Dijkstra 算法,得到 $u$ 到所有顶点的最短距离 $d'(u, v)$; +5. 最终结果还原为原图权重:$d(u, v) = d'(u, v) - h(u) + h(v)$,即为原图中 $u$ 到 $v$ 的最短路径长度。 ### 3.3 Johnson 算法的实现代码 @@ -119,76 +122,69 @@ from collections import defaultdict import heapq def johnson(graph, n): - # 添加新顶点 s + """ + Johnson 算法:多源最短路径,支持负权边但不支持负权环。 + :param graph: 邻接表,graph[u] = {v: w, ...},节点编号 0~n-1 + :param n: 节点总数 + :return: dist 矩阵,dist[i][j] 表示 i 到 j 的最短路径长度;若有负权环返回 None + """ + # 1. 构建新图,添加超级源点 s(编号为 n),从 s 向所有顶点连权重为 0 的边 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) + for u in range(n): + new_graph[n][u] = 0 # s -> u,权重为 0 + + # 2. Bellman-Ford 算法,计算超级源点 s 到每个顶点的最短距离 h(v) h = [float('inf')] * (n + 1) - h[n] = 0 - + h[n] = 0 # s 到自身距离为 0 + # 最多 n 轮松弛 for _ in range(n): + updated = False for u in new_graph: for v, w in new_graph[u].items(): - if h[v] > h[u] + w: + if h[u] != float('inf') and h[v] > h[u] + w: h[v] = h[u] + w - - # 检查是否存在负权环 + updated = True + if not updated: + break + + # 检查负权环:如果还能松弛,说明有负环 for u in new_graph: for v, w in new_graph[u].items(): - if h[v] > h[u] + w: + if h[u] != float('inf') and h[v] > h[u] + w: return None # 存在负权环 - - # 重新赋权 + + # 3. 重新赋权:w'(u,v) = w(u,v) + h[u] - h[v],保证所有边权非负 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)] + + # 4. 对每个顶点运行 Dijkstra 算法,计算最短路径 + dist = [[float('inf')] * 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: + heap = [(0, source)] + visited = [False] * n + while heap: + cur_dist, u = heapq.heappop(heap) + if visited[u]: continue - visited.add(u) - + visited[u] = True 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)) - - # 转换回原始权重 + if d[v] > cur_dist + w: + d[v] = cur_dist + w + heapq.heappush(heap, (d[v], v)) + # 5. 还原原图权重 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)$ @@ -197,9 +193,8 @@ def johnson(graph, n): - 因此总时间复杂度为 $O(VE \log V)$ - **空间复杂度**:$O(V^2)$ - - 需要存储距离矩阵,大小为 $O(V^2)$ - - 需要存储重新赋权后的图,大小为 $O(E)$ - - 因此总空间复杂度为 $O(V^2)$ + - 主要空间消耗在距离矩阵($O(V^2)$)以及重新赋权后的图($O(E)$)。 + - 因此总体空间复杂度为 $O(V^2)$。 ## 练习题目 From 4e64ac3a8b96e79f6efd27cb7f075aab13b6132f Mon Sep 17 00:00:00 2001 From: ITCharge Date: Mon, 22 Sep 2025 15:19:47 +0800 Subject: [PATCH 4/5] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=92=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E8=AF=AD=E5=8F=A5=E8=A1=A8=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../06_10_graph_the_second_shortest_path.md | 186 +++++++++--------- 1 file changed, 88 insertions(+), 98 deletions(-) diff --git a/docs/06_graph/06_10_graph_the_second_shortest_path.md b/docs/06_graph/06_10_graph_the_second_shortest_path.md index 47864e21..3182fb57 100644 --- a/docs/06_graph/06_10_graph_the_second_shortest_path.md +++ b/docs/06_graph/06_10_graph_the_second_shortest_path.md @@ -1,137 +1,127 @@ -## 1.1 次短路径简介 +## 1. 次短路径简介 -> **次短路径**:给定一个带权有向图,求从起点到终点的次短路径。次短路径是指长度严格大于最短路径的所有路径中长度最小的那条路径。 +> **次短路径(Second Shortest Path)**:指从起点到终点的所有简单路径中,路径总权值严格大于最短路径、且在此条件下最小的那条路径。 -### 1.1.1 问题特点 +次短路径的本质,是在所有从起点到终点的路径中,找到一条「长度严格大于最短路径」但又尽可能短的路径。换句话说,最短路径是最优解,次短路径则是在排除所有最优解(即所有与最短路径等长的路径)后,找到的次优解。 -- 次短路径必须严格大于最短路径 -- 可能存在多条最短路径,但次短路径是唯一的 -- 如果不存在次短路径(如最短路径是唯一的),则返回 $-1$。 +这种问题在实际生活和工程中非常常见,主要应用于需要「备用方案」或「容错能力」的场景。例如: -### 1.1.2 常见变体 +- **网络路由**:当主路由失效时,快速切换到次短路径,保证数据传输不中断。 +- **交通导航与物流**:为司机或配送员提供绕行路线,规避拥堵或突发状况。 +- **通信网络**:设计冗余链路,提高网络的健壮性和可靠性。 +- **算法竞赛**:常见「求第 2 优解」类问题,或需要多方案备选时。 -1. 允许重复边的次短路径 -2. 不允许重复边的次短路径 -3. 带约束条件的次短路径(如必须经过某些节点) +> **注意**:本文默认边权非负(与 Dijkstra 条件一致)。如果有负权边,请使用适配的算法并谨慎处理。 -## 1.2 次短路径基本思路 +## 2. 次短路径常见解法 -求解次短路径的常用方法是使用 Dijkstra 算法的变体。基本思路如下: +在实际问题中,寻找次短路径(即严格大于最短路径的最优路径)通常需要对经典的最短路算法进行适当扩展。最常用且高效的方法是基于 Dijkstra 算法的变体。 -1. 使用 Dijkstra 算法找到最短路径。 -2. 在寻找最短路径的过程中,同时维护次短路径。 -3. 对于每个节点,我们需要维护两个距离值: - - $dist1[u]$:从起点到节点 u 的最短距离。 - - $dist2[u]$:从起点到节点 u 的次短距离。 +### 2.1 扩展版 Dijkstra 的核心思路 -### 1.2.1 具体实现步骤 +> **扩展版 Dijkstra 的核心思路**: +> +> 在使用优先队列寻找最短路的过程中,同时为每个节点记录两条路径长度:一条是目前已知的最短路径,另一条是比最短路径严格更长、但次优的路径。每次处理节点时,尝试用新路径更新这两条记录:如果新路径比当前最短路径还短,就把原最短路径作为次短路径,并更新最短路径;如果新路径介于最短和次短之间,就更新次短路径。其余情况直接跳过。最终,终点的次短路径记录就是所求答案。 -1. 初始化 $dist1$ 和 $dist2$ 数组,所有值设为无穷大。 -2. 将起点加入优先队列,距离为 $0$。 -3. 每次从队列中取出距离最小的节点 $u$。 -4. 遍历 $u$ 的所有邻接节点 $v$: - - 如果找到更短的路径,更新 $dist1[v]$。 - - 如果找到次短的路径,更新 $dist2[v]$。 -5. 最终 $dist2[target]$ 即为所求的次短路径长度。 +这种方法思路清晰、实现简单,是解决次短路径问题的主流方案。 -### 1.2.2 算法正确性证明 +### 2.2 扩展版 Dijkstra 的具体步骤 -1. 对于任意节点 $u$,$dist1[u]$ 一定是最短路径长度。 -2. 对于任意节点 $u$,$dist2[u]$ 一定是次短路径长度。 -3. 算法会考虑所有可能的路径,因此不会遗漏次短路径。 +1. 初始化 $dist1$ 和 $dist2$ 数组,全部赋值为无穷大(表示尚未到达)。 +2. 将起点的 $dist1$ 设为 $0$,并将起点以距离 $0$ 加入优先队列。 +3. 每次从优先队列中取出距离最小的节点 $u$。 +4. 枚举 $u$ 的所有邻接节点 $v$,尝试用 $u$ 的当前路径更新 $v$ 的最短和次短距离: + - 如果新路径长度小于 $dist1[v]$,则将 $dist1[v]$ 的原值赋给 $dist2[v]$,并用新路径更新 $dist1[v]$,同时将 $v$ 及其新距离加入队列。 + - 如果新路径长度介于 $dist1[v]$ 与 $dist2[v]$ 之间(即 $dist1[v] <$ 新路径 $< dist2[v]$),则用新路径更新 $dist2[v]$,并将 $v$ 及其新距离加入队列。 +5. 算法结束后,$dist2[target]$ 即为所求的次短路径长度(如果为无穷大则表示不存在)。 -## 1.3 次短路径代码实现 +### 2.3 扩展版 Dijkstra 的代码实现 ```python import heapq +from collections import defaultdict -def second_shortest_path(n: int, edges: List[List[int]], start: int, end: int) -> int: +def second_shortest_path(n, edges, s, t): """ - 计算从起点到终点的次短路径长度 - - 参数: - n: 节点数量 - edges: 边列表,每个元素为 [起点, 终点, 权重] - start: 起始节点 - end: 目标节点 - - 返回: - 次短路径的长度,如果不存在则返回 -1 + 求解有向/无向图中从 s 到 t 的次短路径长度(严格大于最短路径的最小路径)。 + 参数说明: + n: 节点数(编号 0 ~ n - 1) + edges: List[(u, v, w)],每条边 (u, v, w) 表示 u 到 v 有一条权重为 w 的边 + s: 起点编号 + t: 终点编号 + 返回: + s 到 t 的次短路径长度,若不存在返回 float('inf') + 注意: + - 默认边权非负 + - 如果为无向图,请取消 graph[v].append((u, w))的注释 """ # 构建邻接表 - graph = [[] for _ in range(n)] + graph = defaultdict(list) 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)] - + # 如果是无向图,取消下行注释 + # graph[v].append((u, w)) + + INF = float('inf') + dist1 = [INF] * n # dist1[i]:s 到 i 的最短路径长度 + dist2 = [INF] * n # dist2[i]:s 到 i 的严格次短路径长度 + + dist1[s] = 0 + # 优先队列,元素为(当前路径长度, 节点编号) + pq = [(0, s)] + while pq: d, u = heapq.heappop(pq) - - # 如果当前距离大于次短距离,跳过 + # 剪枝:如果当前弹出的距离已大于该点的次短路,则无需处理 if d > dist2[u]: continue - - # 遍历所有邻接节点 + # 遍历 u 的所有邻居 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}") + nd = d + w # 新的路径长度 + # 如果找到更短的路径,更新最短和次短 + if nd < dist1[v]: + dist2[v] = dist1[v] + dist1[v] = nd + heapq.heappush(pq, (dist1[v], v)) + # 如果新路径严格介于最短和次短之间,更新次短 + elif dist1[v] < nd < dist2[v]: + dist2[v] = nd + heapq.heappush(pq, (dist2[v], v)) + # 其他情况(如 nd 等于 dist1[v] 或大于等于 dist2[v])无需处理 + + return dist2[t] # 如果为 INF 表示不存在次短路径 ``` -## 1.4 算法复杂度分析 +### 2.4 扩展版 Dijkstra 算法分析 + +- **时间复杂度**:$O((V + E)\log V)$,其中 $V$ 是节点数,$E$ 是边数。与 Dijkstra 同阶,常数略大,因为每点维护两条距离。 +- **空间复杂度**:$O(V)$,用于存储距离数组和优先队列。 + +## 3. 进阶与常见问题 + +### 3.1 无权图 / 单位权图的次短路径 -- 时间复杂度:$O((V + E)\log V)$,其中 $V$ 是节点数,$E$ 是边数。 -- 空间复杂度:$O(V)$,用于存储距离数组和优先队列。 +对于无权图或所有边权均为 $1$ 的单位权图,求次短路径时可以采用 BFS(广度优先搜索)思想,同样维护两个距离数组:$dist1$ 表示最短路径,$dist2$ 表示严格次短路径。使用普通队列按层推进,每当遇到更短路径或介于最短和次短之间的路径时,及时更新对应的距离并将节点入队。整体实现思路与扩展版 Dijkstra 类似,只是优先队列换成了普通队列。 -## 1.5 应用场景 +### 3.2 与 K 短路问题的关系 -1. 网络路由:寻找备用路径。 -2. 交通规划:寻找替代路线。 -3. 通信网络:寻找备用通信路径。 -4. 物流配送:规划备用配送路线。 +次短路径实际上是 K 短路问题在 $K = 2$ 时的特例。经典的 K 短路算法有 Yen 算法、Eppstein 算法等,但在 $K = 2$ 的场景下,直接用「扩展版 Dijkstra 维护两条距离」往往更简单高效,代码实现也更直观。 -## 1.6 注意事项 +### 3.3 常见易错点与细节说明 -1. 次短路径必须严格大于最短路径。 -2. 如果不存在次短路径,返回 $-1$。 -3. 图中可能存在负权边,此时需要使用 Bellman-Ford 算法的变体。 -4. 对于无向图,需要将每条边都加入两次。 +- **严格大于最短路径**:次短路径必须严格大于最短路径。如果不存在严格大于最短路径的方案,应返回 $-1$。如果题目允许等长但不同路径作为次短路径,需根据题意调整实现。 +- **松弛顺序问题**:当 `nd < dist1[v]` 时,必须先将原有的 `dist1[v]` 赋值给 `dist2[v]`,再更新 `dist1[v]`,否则会丢失正确的次短路径信息。 +- **重复 / 等长路径处理**:当 `nd == dist1[v]` 时,通常不应更新 `dist2[v]`(除非题目特别说明等长但不同路径也算次短路径)。 +- **边权要求**:本算法默认所有边权为非负。如果存在负权边,需使用 Bellman-Ford 算法的变体,并仔细验证实现的正确性。 +- **有向图与无向图的区别**:注意区分有向图和无向图。对于无向图,构图时每条边需正反各加入一次,避免遗漏路径。 ## 练习题目 - [2045. 到达目的地的第二短时间](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2000-2099/second-minimum-time-to-reach-destination.md) -- [次短路径题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E6%AC%A1%E7%9F%AD%E8%B7%AF%E5%BE%84%E9%A2%98%E7%9B%AE) \ No newline at end of file +- [次短路径题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E6%AC%A1%E7%9F%AD%E8%B7%AF%E5%BE%84%E9%A2%98%E7%9B%AE) + + + + + From c54a2d459a4b2a93c9094ac86f188cf239b4d932 Mon Sep 17 00:00:00 2001 From: ITCharge Date: Mon, 22 Sep 2025 15:20:22 +0800 Subject: [PATCH 5/5] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=92=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E8=AF=AD=E5=8F=A5=E8=A1=A8=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/06_graph/06_11_graph_bipartite_basic.md | 73 ++++ ..._graph_system_of_difference_constraints.md | 127 ------- docs/06_graph/06_12_graph_bipartite_basic.md | 78 ---- .../06_12_graph_bipartite_matching.md | 348 ++++++++++++++++++ .../06_13_graph_bipartite_matching.md | 285 -------------- docs/06_graph/index.md | 5 +- 6 files changed, 423 insertions(+), 493 deletions(-) create mode 100644 docs/06_graph/06_11_graph_bipartite_basic.md delete mode 100644 docs/06_graph/06_11_graph_system_of_difference_constraints.md delete mode 100644 docs/06_graph/06_12_graph_bipartite_basic.md create mode 100644 docs/06_graph/06_12_graph_bipartite_matching.md delete mode 100644 docs/06_graph/06_13_graph_bipartite_matching.md diff --git a/docs/06_graph/06_11_graph_bipartite_basic.md b/docs/06_graph/06_11_graph_bipartite_basic.md new file mode 100644 index 00000000..a6bc5b7f --- /dev/null +++ b/docs/06_graph/06_11_graph_bipartite_basic.md @@ -0,0 +1,73 @@ +## 1. 二分图简介 + +> **二分图(Bipartite Graph)**:又称「二部图」,是一类特殊的无向图。其顶点集可以被划分为两个互不重叠的子集,且所有边仅连接这两个子集之间的顶点,同一子集内的顶点之间没有边相连。 + +直观地说,就是「左边只连右边,左边不互连;右边只连左边,右边不互连」。 + +## 2. 二分图判定 + +> **二分图判定**:判断一个无向图是否可以将所有顶点划分为两个互不重叠的集合,使得每条边的两个端点都分别属于不同的集合。换句话说,图中不存在奇数长度的环,则该图为二分图。 + +### 2.1 二分图判定的具体步骤 + +判断一个无向图是否为二分图,常用的方法是「染色法」:通过给图的每个顶点染上两种不同的颜色,检查是否能做到每条边的两个端点颜色不同。具体步骤如下: + +1. **初始化染色数组**:为每个顶点分配颜色标记,初始均为未染色(如 0 表示未染色,1 和 -1 分别代表两种颜色)。 +2. **遍历所有顶点**:对每个未染色的顶点,执行一次 BFS 或 DFS 染色(因图可能不连通,需分别处理每个连通分量)。 +3. **染色与冲突检测**: + - 从当前顶点开始,赋予一种颜色(如 1)。 + - 遍历其所有邻接点: + - 如果邻接点未染色,则染为相反颜色(-1),并递归 / 迭代继续处理; + - 如果邻接点已染色且与当前顶点颜色相同,则发生冲突,说明不是二分图,立即返回 `False`。 +4. **全部顶点染色无冲突**:如果所有顶点均成功染色且未出现冲突,则该图为二分图。 + + +### 2.2 二分图判定的代码实现 + +```python +def is_bipartite(graph): + """ + 判断无向图是否为二分图(染色法) + :param graph: List[List[int]],邻接表表示的无向图 + :return: bool,是否为二分图 + """ + n = len(graph) + colors = [0] * n # 0 表示未染色,1 和 -1 表示两种颜色 + + def dfs(node, color): + """ + 对节点 node 进行染色,并递归染色其所有邻居 + :param node: 当前节点编号 + :param color: 当前节点应染的颜色(1 或 -1) + :return: bool,若染色无冲突返回 True,否则 False + """ + colors[node] = color # 给当前节点染色 + for neighbor in graph[node]: + if colors[neighbor] == color: + # 邻居和当前节点颜色相同,冲突,非二分图 + return False + if colors[neighbor] == 0: + # 邻居未染色,递归染成相反颜色 + if not dfs(neighbor, -color): + return False + return True + + for i in range(n): + if colors[i] == 0: + # 只对未染色的节点(新连通分量)进行 DFS 染色 + if not dfs(i, 1): + return False # 染色过程中发现冲突,非二分图 + return True # 所有节点染色无冲突,是二分图 +``` + +### 2.3 二分图判定的算法分析 + +- **时间复杂度**:$O(V + E)$,其中 $V$ 为顶点数,$E$ 为边数。每个顶点和每条边最多被访问一次。 +- **空间复杂度**:$O(V)$,主要用于存储颜色数组和递归/队列。 + + +## 练习题目 + +- [0785. 判断二分图](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/is-graph-bipartite.md) + +- [二分图基础题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E4%BA%8C%E5%88%86%E5%9B%BE%E5%9F%BA%E7%A1%80%E9%A2%98%E7%9B%AE) \ No newline at end of file diff --git a/docs/06_graph/06_11_graph_system_of_difference_constraints.md b/docs/06_graph/06_11_graph_system_of_difference_constraints.md deleted file mode 100644 index 8a3b184b..00000000 --- a/docs/06_graph/06_11_graph_system_of_difference_constraints.md +++ /dev/null @@ -1,127 +0,0 @@ -## 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. 对于大规模问题,可以考虑使用其他优化算法 - -## 练习题目 - -- [0995. K 连续位的最小翻转次数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/minimum-number-of-k-consecutive-bit-flips.md) -- [1109. 航班预订统计](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/corporate-flight-bookings.md) - -- [差分约束系统题目列表](https://github.com/itcharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E5%B7%AE%E5%88%86%E7%BA%A6%E6%9D%9F%E7%B3%BB%E7%BB%9F%E9%A2%98%E7%9B%AE) \ No newline at end of file diff --git a/docs/06_graph/06_12_graph_bipartite_basic.md b/docs/06_graph/06_12_graph_bipartite_basic.md deleted file mode 100644 index 65824e5e..00000000 --- a/docs/06_graph/06_12_graph_bipartite_basic.md +++ /dev/null @@ -1,78 +0,0 @@ -## 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. 处理有向图时,需要先将其转换为无向图再判断是否为二分图 - -## 练习题目 - -- [0785. 判断二分图](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/is-graph-bipartite.md) - -- [二分图基础题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E4%BA%8C%E5%88%86%E5%9B%BE%E5%9F%BA%E7%A1%80%E9%A2%98%E7%9B%AE) \ No newline at end of file diff --git a/docs/06_graph/06_12_graph_bipartite_matching.md b/docs/06_graph/06_12_graph_bipartite_matching.md new file mode 100644 index 00000000..84c09f65 --- /dev/null +++ b/docs/06_graph/06_12_graph_bipartite_matching.md @@ -0,0 +1,348 @@ +## 1. 二分图最大匹配简介 + +> **二分图最大匹配(Maximum Bipartite Matching)**:图论中的一个基础且重要的问题。其目标是在一个二分图中,找到一组两两不相交的边,使得被匹配的点对数量最大。 + +- **匹配**:在二分图中,选择若干条边,使得这些边之间没有公共端点(即每个点最多只参与一条匹配边)。 +- **最大匹配**:在所有可能的匹配中,选出包含边数最多的那一组,即让尽可能多的点被配对。 + +二分图最大匹配问题有多种经典算法,常用的有以下三类: + +**1. 匈牙利算法(Hungarian Algorithm,DFS 增广路)**: + +通过不断为未匹配的左侧点寻找「增广路」来扩展匹配的经典方法。它通常采用深度优先搜索(DFS)递归实现,代码简洁,易于理解,适合小中规模的数据场景。其时间复杂度为 $O(VE)$,实现简单,适合竞赛和工程快速上手,但在大规模稠密图下效率有限。 + +**2. Hopcroft-Karp 算法**: + +通过分层 BFS 批量寻找多条增广路,每轮可以一次性扩展多组匹配,从而大幅提升效率。该算法先用 BFS 对图进行分层,再用 DFS 在分层图中寻找增广路,适合处理大规模稠密二分图。其时间复杂度为 $O(\sqrt{V}E)$,效率高,但实现相对匈牙利算法更为复杂。 + +**3. 网络流算法(最大流建模)**: + +将二分图最大匹配问题转化为最大流问题:左侧点连接源点,右侧点连接汇点,边容量为 1,最大流即为最大匹配。常用的最大流算法有 Ford-Fulkerson、Edmonds-Karp、Dinic、ISAP 等。其时间复杂度依赖具体算法,Dinic 算法常见为 $O(\min(V^{2/3}, E^{1/2}) \cdot E)$。该方法可扩展到带权匹配、带容量等复杂约束,适合工程和综合性问题,但实现和调试相对繁琐。 + +## 2. 匈牙利算法 + +### 2.1 增广路介绍 + +在介绍匈牙利算法之前,先理解「增广路」的概念: + +> **增广路(Augmenting Path)**:在当前的匹配状态下,从某个尚未匹配的左侧点出发,沿着图中的边依次前进,最终到达一个同样未被匹配的右侧点。要求这条路径上的边类型要交替出现:先走一条未被匹配的边,再走一条已被匹配的边,如此反复,直到终点。只要存在这样的路径,我们就可以把路径上边的匹配状态「翻转」——原本未匹配的边变为匹配,原本已匹配的边变为未匹配,从而让整体匹配数增加 $1$。 + +增广路的本质是「为未匹配的点找到一条可以扩展匹配的通路」。匈牙利算法正是不断寻找增广路,并沿着增广路调整匹配关系,从而逐步扩大整体匹配规模。 + +下面详细介绍匈牙利算法的基本思想。 + +### 2.2 匈牙利算法的基本思想 + +> **匈牙利算法的基本思想**: +> +> 不断为左侧集合中尚未匹配的点寻找一条「增广路」,即从该点出发,通过深度优先搜索(DFS)探索一条起点和终点均为未匹配点、且匹配边与非匹配边交替出现的路径。如果找到这样的路径,就沿路径翻转匹配关系,使整体匹配数增加 1。重复这一过程,直到所有未匹配点都无法再找到增广路为止,最终得到最大匹配。 + +### 2.3 匈牙利算法的具体步骤 + +匈牙利算法的核心流程可以分为以下几个步骤: + +1. **初始化匹配关系**:一开始,右侧所有点都没有被匹配(比如用 `match_right = [-1] * right_size` 表示,-1 代表未匹配)。 +2. **依次为每个左侧点找对象**:遍历左侧集合的每个点 $u$,尝试为它找到一条「增广路」来配对。 +3. **用 DFS 寻找增广路**: + - 对当前左侧点 $u$,依次考察它能连到的每个右侧点 $v$: + - 如果 $v$ 这轮还没被访问过,先标记已访问,避免重复。 + - 如果 $v$ 还没有配对,或者 $v$ 已配对的左侧点 $u'$ 能继续递归找到新的增广路,那么就让 $u$ 和 $v$ 配对(即 `match_right[v] = u`),并返回成功。 + - 如果所有邻接的右侧点都无法配对,则返回失败。 +4. **重复上述过程**: + - 每当成功为一个左侧点找到增广路并配对,整体匹配数加 1。 + - 继续尝试下一个左侧点,直到所有左侧点都无法再增广为止。 +5. **输出最大匹配数**:最后统计一共配对了多少组,这个数就是二分图的最大匹配数。 + +### 2.4 匈牙利算法的代码实现 + +```python +def max_bipartite_matching(graph, left_size, right_size): + """ + 二分图最大匹配(匈牙利算法,DFS 增广路实现) + :param graph: 邻接表,graph[u] 存储左侧点 u 能连接到的所有右侧点编号(如 [[], [0,2], ...]) + :param left_size: 左侧点个数 + :param right_size: 右侧点个数 + :return: 最大匹配数 + """ + match_right = [-1] * right_size # 记录每个右侧点当前匹配到的左侧点编号,-1 表示未匹配 + result = 0 # 匹配数 + + for left in range(left_size): + visited = [False] * right_size # 每次为一个左侧点增广时,重置右侧点访问标记 + if find_augmenting_path(graph, left, visited, match_right): + result += 1 # 成功增广,匹配数加一 + + return result + +def find_augmenting_path(graph, left, visited, match_right): + """ + 尝试为左侧点 left 寻找一条增广路 + :param graph: 邻接表 + :param left: 当前尝试增广的左侧点编号 + :param visited: 右侧点访问标记,防止重复访问 + :param match_right: 右侧点的匹配关系 + :return: 是否找到增广路 + """ + for right in graph[left]: # 遍历 left 能连接到的所有右侧点 + if 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.5 匈牙利算法的算法分析 + +- **时间复杂度**:O(VE),其中 V 表示顶点数,E 表示边数。每次尝试增广时,最坏情况下需要遍历所有边,整体复杂度为 O(VE)。 +- **空间复杂度**: + - 如果使用邻接矩阵存储图,空间复杂度为 O(V²)。 + - 如果使用邻接表存储图,空间复杂度为 O(V + E)。 + +## 3. Hopcroft-Karp 算法 + +### 3.1 Hopcroft-Karp 算法的基本思想 + +> **Hopcroft-Karp 算法的基本思想**: +> +> 每轮通过 BFS 对图进行分层,快速找到所有最短的不相交增广路,然后用 DFS 同时增广多条路径,从而大幅提升匹配效率。与传统的匈牙利算法每次只增广一条路径不同,Hopcroft-Karp 算法每轮能批量增广多条路径,极大减少了总的增广次数,因此在大规模二分图上表现尤为优越。 + +### 3.2 Hopcroft-Karp 算法的具体步骤 + +#### Hopcroft-Karp 算法的具体步骤 + +Hopcroft-Karp 算法高效求解二分图最大匹配,其核心思想是 **分层批量增广**,每轮同时增广多条最短增广路,极大提升效率。具体流程如下: + +1. **初始化匹配关系**:所有点一开始都没有匹配对象。 +2. **分层(BFS)**: + - 首先,对所有未匹配的左侧点进行广度优先搜索(BFS),给每个左侧点分配一个「层号」。 + - 在搜索过程中,只能沿着「未匹配的边 → 已匹配的边」交替前进,这样就能构建出分层图。 + - 如果在 BFS 过程中遇到未匹配的右侧点,说明找到了增广路,并记录下最短的增广路长度。 +3. **批量增广(DFS)**: + - 接下来,对所有未匹配的左侧点,按照分层图,用深度优先搜索(DFS)去找增广路。 + - 只在层号递增的方向递归查找,找到一条增广路就立即进行匹配。 + - 这样每一轮可以同时增广多条互不重叠的最短增广路,大大提高效率。 +4. **重复迭代**: + - 如果本轮找到了增广路,就更新匹配关系,然后回到第 $2$ 步,继续分层和增广。 + - 如果本轮没有找到任何增广路,算法结束,此时的匹配就是最大匹配。 + +### 3.3 Hopcroft-Karp 算法的代码实现 + +```python +from collections import deque + +def hopcroft_karp(graph, left_size, right_size): + """ + Hopcroft-Karp 算法求二分图最大匹配 + :param graph: List[List[int]],graph[i] 存储左侧点 i 能连到的所有右侧点编号 + :param left_size: 左侧点数量 + :param right_size: 右侧点数量 + :return: 最大匹配数 + """ + # match_left[i] = j 表示左侧点 i 匹配到右侧点 j,未匹配为 -1 + match_left = [-1] * left_size + # match_right[j] = i 表示右侧点 j 匹配到左侧点 i,未匹配为 -1 + match_right = [-1] * right_size + result = 0 # 匹配数 + + while True: + # 1. BFS 分层,dist[i] 表示左侧点 i 到未匹配状态的最短距离 + dist = [-1] * left_size + queue = deque() + for i in range(left_size): + if match_left[i] == -1: + dist[i] = 0 + queue.append(i) + # 标记本轮是否存在增广路 + found_augmenting = False + + while queue: + u = queue.popleft() + for v in graph[u]: + if match_right[v] == -1: + # 右侧点 v 未匹配,说明存在增广路 + found_augmenting = True + elif dist[match_right[v]] == -1: + # 沿着匹配边走,分层 + dist[match_right[v]] = dist[u] + 1 + queue.append(match_right[v]) + + if not found_augmenting: + # 没有增广路,算法结束 + break + + # 2. DFS 尝试批量增广 + def dfs(u): + for v in graph[u]: + # 如果右侧点未匹配,或者可以沿着分层图递归找到增广路 + if match_right[v] == -1 or (dist[match_right[v]] == dist[u] + 1 and dfs(match_right[v])): + match_left[u] = v + match_right[v] = u + return True + # 没有找到增广路 + return False + + # 3. 对所有未匹配的左侧点尝试增广 + for i in range(left_size): + if match_left[i] == -1: + if dfs(i): + result += 1 + + return result +``` + +### 3.4 Hopcroft-Karp 算法的算法分析 + +- **时间复杂度**:$O(\sqrt{V}E)$,其中 $V$ 为顶点数,$E$ 为边数。相比传统匈牙利算法,Hopcroft-Karp 算法通过分层批量增广,大幅减少了增广路的查找次数,提升了整体效率。 +- **空间复杂度**:$O(V + E)$,主要用于存储邻接表、匹配关系和辅助队列等数据结构。 + +### 3.5 Hopcroft-Karp 算法优化 + +1. **双向 BFS**:从左右两侧同时进行 BFS,进一步缩小搜索范围,加快分层过程。 +2. **动态分层策略**:根据当前的匹配情况灵活调整分层方式,提高增广路查找效率。 +3. **贪心预处理**:在正式执行算法前,先用贪心策略进行初步匹配,为后续批量增广打下基础。 +4. **并行与分布式优化**:利用多线程或分布式计算资源,实现 BFS/DFS 的并行处理,提升整体运行速度。 + +## 4. 网络流算法 + +### 4.1 网络流算法的基本思想 + +二分图最大匹配问题可以巧妙地转化为网络流中的最大流问题来求解。其核心思想如下: + +- **建模方式**:将二分图的左侧点、右侧点分别作为网络中的两类节点,额外引入一个「源点」和一个「汇点」。 + - 源点 $S$ 向所有左侧点连一条容量为 $1$ 的有向边。 + - 所有右侧点向汇点 $T$ 连一条容量为 $1$ 的有向边。 + - 原二分图中每条左侧点 $u$ 到右侧点 $v$ 的边,建一条 $u \to v$ 的有向边,容量为 $1$。 +- **流量含义**:每条边的容量为 $1$,表示每个点最多只能参与一次匹配。网络中从 $S$ 到 $T$ 的最大流量,恰好等于二分图的最大匹配数。 +- **求解过程**:使用最大流算法(如 Ford-Fulkerson、Edmonds-Karp、Dinic 等)在该网络上求 $S$ 到 $T$ 的最大流,流量的大小即为最大匹配数。 + + +> **网络流算法的直观理解**: +> +> 每当在网络中找到一条从 $S$ 到 $T$ 的增广路径,就等价于在原二分图中新增一对匹配。由于每个点与源点或汇点之间的边容量均为 $1$,确保每个点最多只参与一次匹配,严格满足二分图匹配的要求。 + + +### 4.2 网络流算法的具体步骤 + +### 4.2 网络流算法的具体步骤 + +将二分图最大匹配问题转化为最大流问题,通常分为以下几个步骤: + +1. **引入源点与汇点**:新增源点 $S$ 和汇点 $T$。 +2. **源点连向左侧所有点**:从 $S$ 向每个左侧点各连一条容量为 $1$ 的有向边,表示每个左侧点最多参与一次匹配。 +3. **右侧所有点连向汇点**:从每个右侧点向 $T$ 各连一条容量为 $1$ 的有向边,表示每个右侧点最多参与一次匹配。 +4. **原二分图边建模**:对于原图中每条左侧点 $u$ 到右侧点 $v$ 的边,添加 $u \to v$ 的有向边,容量为 $1$。 +5. **执行最大流算法**:在该网络上运行最大流算法(如 Edmonds-Karp、Dinic 等),计算 $S$ 到 $T$ 的最大流量。 +6. **得到最大匹配数**:最大流的数值即为原二分图的最大匹配数。 + +### 4.3 网络流算法的代码实现 + +```python +from collections import defaultdict, deque + +def max_flow_bipartite_matching(graph, left_size, right_size): + """ + 使用网络流(Ford-Fulkerson 算法)求解二分图最大匹配 + :param graph: List[List[int]],左侧每个点可连的右侧点编号列表 + :param left_size: 左侧点个数 + :param right_size: 右侧点个数 + :return: 最大匹配数 + """ + # 构建网络流图,节点编号: + # 0 ~ left_size-1:左侧点 + # left_size ~ left_size+right_size-1:右侧点 + # source: left_size+right_size + # sink: left_size+right_size+1 + flow_graph = defaultdict(dict) + source = left_size + right_size + sink = source + 1 + + # 源点到左侧点,容量为 1 + for i in range(left_size): + flow_graph[source][i] = 1 + flow_graph[i][source] = 0 # 反向边,初始为 0 + + # 右侧点到汇点,容量为 1 + for i in range(right_size): + right_node = left_size + i + flow_graph[right_node][sink] = 1 + flow_graph[sink][right_node] = 0 # 反向边 + + # 左侧点到右侧点,容量为 1 + for i in range(left_size): + for j in graph[i]: + right_node = left_size + j + flow_graph[i][right_node] = 1 + flow_graph[right_node][i] = 0 # 反向边 + + def bfs(): + """ + 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 # 没有增广路,算法结束 + + # 计算本次增广路的最小残量(本题均为 1,写全以便扩展) + 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.4 网络流算法的算法分析 + +- **时间复杂度分析**: + 1. **Ford-Fulkerson 算法**:$O(VE^2)$。该算法每次通过 DFS/BFS 寻找一条增广路,每次最多增加 1 单位流量,最坏情况下需要 $O(E)$ 次增广,每次增广遍历 $O(E)$ 条边,总共 $O(VE^2)$。适合边权为 1 或较小的稀疏图,实际中常用于教学和小规模数据。 + 2. **Dinic 算法**:$O(V^2E)$。Dinic 算法通过分层网络和多路增广优化了增广过程,理论上在一般网络流问题中表现优秀,尤其适合稠密图。对于二分图最大匹配,Dinic 的实际复杂度可降为 $O(\sqrt{V}E)$,但一般网络流场景下为 $O(V^2E)$。 + 3. **ISAP 算法**:$O(V^2E)$。ISAP(Improved Shortest Augmenting Path)算法通过维护距离标号和当前弧优化增广过程,适合大规模稠密图,实际表现优于 Dinic,但理论复杂度同为 $O(V^2E)$。 + +- **空间复杂度**:$O(V + E)$。主要用于存储残量网络(邻接表/矩阵)和辅助数组(如层次、前驱、队列等),与图的规模线性相关。 + + + +## 练习题目 + +- [LCP 04. 覆盖](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCP/broken-board-dominoes.md) +- [1947. 最大兼容性评分和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1900-1999/maximum-compatibility-score-sum.md) +- [1595. 连通两组点的最小成本](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1500-1599/minimum-cost-to-connect-two-groups-of-points.md) + +- [二分图最大匹配题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E4%BA%8C%E5%88%86%E5%9B%BE%E6%9C%80%E5%A4%A7%E5%8C%B9%E9%85%8D%E9%A2%98%E7%9B%AE) \ No newline at end of file diff --git a/docs/06_graph/06_13_graph_bipartite_matching.md b/docs/06_graph/06_13_graph_bipartite_matching.md deleted file mode 100644 index ce29d25d..00000000 --- a/docs/06_graph/06_13_graph_bipartite_matching.md +++ /dev/null @@ -1,285 +0,0 @@ -## 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) - - 优点:可以处理更复杂的问题,如带权匹配 - - 缺点:实现复杂,常数较大 - - 适用场景:带权匹配,复杂约束条件 - -## 练习题目 - -- [LCP 04. 覆盖](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCP/broken-board-dominoes.md) -- [1947. 最大兼容性评分和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1900-1999/maximum-compatibility-score-sum.md) -- [1595. 连通两组点的最小成本](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1500-1599/minimum-cost-to-connect-two-groups-of-points.md) - -- [二分图最大匹配题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E4%BA%8C%E5%88%86%E5%9B%BE%E6%9C%80%E5%A4%A7%E5%8C%B9%E9%85%8D%E9%A2%98%E7%9B%AE) \ No newline at end of file diff --git a/docs/06_graph/index.md b/docs/06_graph/index.md index 249691cd..8685e0e3 100644 --- a/docs/06_graph/index.md +++ b/docs/06_graph/index.md @@ -10,6 +10,5 @@ - [6.8 单源最短路径(二)](https://github.com/ITCharge/AlgoNote/tree/main/docs/06_graph/06_08_graph_shortest_path_02.md) - [6.9 多源最短路径](https://github.com/ITCharge/AlgoNote/tree/main/docs/06_graph/06_09_graph_multi_source_shortest_path.md) - [6.10 次短路径](https://github.com/ITCharge/AlgoNote/tree/main/docs/06_graph/06_10_graph_the_second_shortest_path.md) -- [6.11 差分约束系统](https://github.com/ITCharge/AlgoNote/tree/main/docs/06_graph/06_11_graph_system_of_difference_constraints.md) -- [6.12 二分图基础](https://github.com/ITCharge/AlgoNote/tree/main/docs/06_graph/06_12_graph_bipartite_basic.md) -- [6.13 二分图最大匹配](https://github.com/ITCharge/AlgoNote/tree/main/docs/06_graph/06_13_graph_bipartite_matching.md) +- [6.11 二分图基础](https://github.com/ITCharge/AlgoNote/tree/main/docs/06_graph/06_11_graph_bipartite_basic.md) +- [6.12 二分图最大匹配](https://github.com/ITCharge/AlgoNote/tree/main/docs/06_graph/06_12_graph_bipartite_matching.md)