当前位置: 首页 > news >正文

图--常见面试问题

目录

1. 克隆图(深拷贝)

基本思路

关键点

示例

2. 课程表安排(拓扑排序)

基本思路

关键点

示例

3. 岛屿数量(连通分量)

基本思路

关键点解析

示例

4. 单词接龙(最短路径)

基本思路

关键点解析

示例

复杂度分析

5. 朋友圈(并查集)

基本思路

关键点解析

示例

复杂度分析

6. 网络延迟时间(Dijkstra)

基本思路

关键点解析

示例

复杂度分析

7. 最小高度树(图中心)

基本思路

关键点解析

示例

复杂度分析

8. 重新安排行程(欧拉路径)

基本思路

关键点解析

示例

复杂度分析

9. 除法求值(加权图)

基本思路

关键点解析

示例

复杂度分析

10. 水管网络(最大流)

基本思路

关键点解析

示例

复杂度分析

问题分类总结


1. 克隆图(深拷贝)

问题类型:图的遍历与复制 算法:DFS/BFS + 哈希表 关键点

  • 使用哈希表 visited 存储原节点到拷贝节点的映射
  • 处理邻居时检查是否已克隆 LeetCode 133
def cloneGraph(node):if not node: return None  # 处理空图的情况from collections import dequevisited = {}  # 存储原始节点到克隆节点的映射queue = deque([node])  # BFS队列,初始包含起始节点visited[node] = Node(node.val, [])  # 创建起始节点的克隆while queue:  # 开始BFS遍历n = queue.popleft()  # 取出队列中的第一个节点for neighbor in n.neighbors:  # 遍历当前节点的所有邻居if neighbor not in visited:  # 如果邻居节点还未被克隆visited[neighbor] = Node(neighbor.val, [])  # 创建邻居节点的克隆queue.append(neighbor)  # 将邻居节点加入队列以便后续处理# 将克隆后的邻居节点添加到当前克隆节点的邻居列表中visited[n].neighbors.append(visited[neighbor])return visited[node]  # 返回起始节点的克隆

基本思路

  1. 创建一个哈希表 visited 来记录原始节点和克隆节点的映射关系
  2. 使用队列 queue 来实现 BFS 遍历
  3. 对于每个原始节点,创建对应的克隆节点,并复制其邻居关系

关键点

  1. visited字典:不仅用于记录哪些节点已经被访问过,更重要的是维护原始节点到克隆节点的映射关系。

  2. BFS遍历:确保我们按层次处理所有节点,先处理节点的克隆,再处理其邻居的克隆。

  3. 邻居关系的复制visited[n].neighbors.append(visited[neighbor]) 这行代码确保了克隆节点之间的连接关系与原图一致。

示例

假设原始图如下:

1 -- 2
|    |
4 -- 3

执行过程:

  1. 克隆节点1,加入队列
  2. 处理节点1,克隆其邻居2和4
  3. 处理节点2,克隆其邻居3
  4. 处理节点4,发现3已克隆
  5. 处理节点3,发现所有邻居已克隆

最终得到一个结构与原始图完全相同的新图。

这种方法是图克隆的标准方法,时间复杂度为O(N),空间复杂度为O(N),其中N是图中的节点数。


2. 课程表安排(拓扑排序)

问题类型:DAG的拓扑排序 算法:Kahn's算法(入度表) 应用场景:任务调度、依赖解析 LeetCode 207

def canFinish(numCourses, prerequisites):from collections import deque# 初始化邻接表和入度数组adj = [[] for _ in range(numCourses)]  # 邻接表存储图in_degree = [0] * numCourses  # 记录每个节点的入度# 构建图和入度数组for a, b in prerequisites:adj[b].append(a)  # b是a的先修课程,b→ain_degree[a] += 1  # a的入度+1# 初始化队列,包含所有入度为0的节点queue = deque([i for i in range(numCourses) if in_degree[i] == 0])count = 0  # 记录已处理的节点数# 开始拓扑排序while queue:u = queue.popleft()  # 取出一个入度为0的节点count += 1  # 已处理节点数+1# 处理u的所有邻居for v in adj[u]:in_degree[v] -= 1  # 邻居的入度-1if in_degree[v] == 0:  # 如果入度变为0queue.append(v)  # 加入队列# 如果所有节点都被处理,说明没有环,可以完成所有课程return count == numCourses

基本思路

  1. 构建图的邻接表和入度数组
  2. 将所有入度为0的节点加入队列
  3. 不断从队列中取出节点,减少其邻居的入度
  4. 如果邻居的入度变为0,加入队列
  5. 最后检查是否所有节点都被处理(即是否存在拓扑排序)

关键点

  1. 邻接表adjadj[b]存储所有以b为先修课程的课程列表(即b指向的节点)

  2. 入度数组in_degree:记录每个课程还有多少先修课程未完成

  3. 队列初始化:开始时只有入度为0的课程(没有先修要求的课程)可以学习

  4. 处理过程:每完成一个课程(从队列取出),它的后继课程的入度减1,如果减到0就可以开始学习

  5. 结果判断:如果处理过的课程数等于总课程数,说明可以完成所有课程;否则说明存在环,无法完成

示例

假设有4门课程,先修关系为:

1 → 0
2 → 0
3 → 1
3 → 2

执行过程:

  1. 入度数组:[2, 1, 1, 0](只有课程3的入度为0)
  2. 初始队列:
  3. 处理3,减少1和2的入度,队列变为[1,2]
  4. 处理1,减少0的入度,队列变为
  5. 处理2,减少0的入度,队列变为
  6. 处理0,没有后续课程
  7. count=4,返回True

这个算法的时间复杂度是O(V+E),其中V是课程数,E是先修关系数。


3. 岛屿数量(连通分量)

问题类型:无向图连通分量 算法:DFS/BFS/并查集 优化技巧:原地修改矩阵标记访问 LeetCode 200

def numIslands(grid):def dfs(i, j):# 检查坐标是否有效且当前单元格是陆地if 0 <= i < len(grid) and 0 <= j < len(grid[0]) and grid[i][j] == '1':# 将当前陆地标记为已访问(相当于"沉没"岛屿)grid[i][j] = '0'# 递归访问四个方向的相邻单元格# 使用map和列表展开来简化四个方向的递归调用list(map(dfs, (i+1, i-1, i, i), (j, j, j+1, j-1)))count = 0# 遍历整个网格for i in range(len(grid)):for j in range(len(grid[0])):if grid[i][j] == '1':  # 发现新岛屿dfs(i, j)  # "沉没"整个岛屿count += 1  # 岛屿计数增加return count

基本思路

  1. 遍历网格中的每个单元格
  2. 当发现陆地('1')时,启动DFS将该岛屿所有相连的陆地标记为已访问('0')
  3. 每次发现新岛屿时计数器加1

关键点解析

  1. DFS函数

    • 边界检查:确保(i,j)在网格范围内
    • 陆地检查:只处理值为'1'的单元格
    • 标记已访问:将访问过的陆地改为'0'避免重复计算
    • 四方向递归:巧妙使用map同时处理四个方向(上、下、左、右)
  2. 主循环

    • 遍历每个网格单元格
    • 发现'1'时启动DFS并增加计数
    • DFS会将该岛屿所有相连的'1'都标记为'0'
  3. 巧妙之处

    • list(map(dfs, (i+1,i-1,i,i), (j,j,j+1,j-1))) 等价于:
     dfs(i+1, j)  # 下dfs(i-1, j)  # 上dfs(i, j+1)  # 右dfs(i, j-1)  # 左
  • 使用list()是为了确保map会实际执行(Python3中map是惰性的)

示例

对于输入网格:

[["1","1","0","0","0"],["1","1","0","0","0"],["0","0","1","0","0"],["0","0","0","1","1"]
]

执行过程:

  1. 发现(0,0)的'1',DFS沉没左上角2×2岛屿,count=1
  2. 跳过已访问的(0,1),(1,0),(1,1)
  3. 发现(2,2)的'1',DFS沉没中间岛屿,count=2
  4. 发现(3,3)的'1',DFS沉没右下角2×1岛屿,count=3 最终返回3

该算法的时间复杂度是O(M×N),其中M和N是网格的行数和列数,因为每个单元格最多被访问一次。空间复杂度最坏情况下是O(M×N)(当整个网格都是陆地时递归栈的深度)。


4. 单词接龙(最短路径)

问题类型:无权图最短路径 算法:BFS(双向优化) 预处理:构建通配符字典 LeetCode 127

def ladderLength(beginWord, endWord, wordList):from collections import dequewordSet = set(wordList)  # 转换为集合提高查找效率if endWord not in wordSet:  # 目标单词不在字典中直接返回0return 0queue = deque([(beginWord, 1)])  # 队列存储(当前单词, 步数)while queue:word, step = queue.popleft()if word == endWord:  # 找到目标单词return step# 生成所有可能的相邻单词for i in range(len(word)):  # 遍历单词的每个位置for c in 'abcdefghijklmnopqrstuvwxyz':  # 尝试所有字母new = word[:i] + c + word[i+1:]  # 构造新单词if new in wordSet:  # 新单词在字典中wordSet.remove(new)  # 避免重复访问queue.append((new, step + 1))  # 加入队列return 0  # 无法转换到目标单词

基本思路

  1. 使用BFS逐层探索可能的单词转换
  2. 每次改变单词的一个字母,生成所有可能的相邻单词
  3. 如果生成的单词存在于字典中,则加入队列继续搜索
  4. 找到目标单词时返回当前步数

关键点解析

  1. BFS队列:存储(当前单词, 转换步数),保证按层次搜索

  2. 单词生成

    • 对单词的每个位置(i)
    • 尝试所有可能的字母替换(a-z)
    • 生成新单词new = word[:i] + c + word[i+1:]
  3. 访问控制

    • 使用集合wordSet快速查找
    • 访问后立即从集合中移除,避免重复处理
  4. 终止条件

    • 找到endWord时返回当前步数
    • 队列为空仍未找到则返回0

示例

假设:

  • beginWord = "hit"
  • endWord = "cog"
  • wordList = ["hot","dot","dog","lot","log","cog"]

执行过程:

  1. 初始队列: [("hit",1)]
  2. 处理"hit":生成"h*t"(hot),加入队列[("hot",2)]
  3. 处理"hot":生成"*ot"(dot,lot),加入队列[("dot",3), ("lot",3)]
  4. 处理"dot":生成"do*"(dog),加入队列[("lot",3), ("dog",4)]
  5. 处理"lot":生成"lo*"(log),加入队列[("dog",4), ("log",4)]
  6. 处理"dog":生成"*og"(cog),找到目标,返回5

复杂度分析

  • 时间复杂度:O(M×N),其中M是单词长度,N是字典大小
  • 空间复杂度:O(N),用于存储队列和单词集合

这种解法保证了找到的是最短转换序列,因为BFS总是优先探索距离起点更近的节点。


5. 朋友圈(并查集)

问题类型:连通性问题 算法:并查集(路径压缩+按秩合并) LeetCode 547

def findCircleNum(isConnected):n = len(isConnected)  # 城市数量parent = list(range(n))  # 初始化父节点数组,每个城市指向自己# 查找根节点(带路径压缩)def find(u):while parent[u] != u:  # 当前节点不是根节点parent[u] = parent[parent[u]]  # 路径压缩:将当前节点指向祖父节点u = parent[u]  # 向上移动到父节点return u  # 返回根节点# 遍历邻接矩阵for i in range(n):for j in range(i+1, n):  # 只需遍历矩阵右上部分,避免重复处理if isConnected[i][j]:  # 如果城市i和j相连parent[find(j)] = find(i)  # 合并两个城市# 统计不同根节点的数量return len(set(map(find, parent)))

基本思路

  1. 初始化每个城市为自己的父节点(即每个城市最初是一个独立的省份)
  2. 遍历邻接矩阵,合并相连的城市
  3. 最后统计不同根节点的数量即为省份总数

关键点解析

  1. 并查集结构

    • parent数组记录每个节点的父节点
    • find函数查找根节点,并进行路径压缩优化
  2. 合并操作

    • 当城市i和j相连时,将j的根节点指向i的根节点
    • parent[find(j)] = find(i)实现了高效的合并
  3. 路径压缩

    • parent[u] = parent[parent[u]]将节点直接指向祖父节点
    • 平摊时间复杂度接近O(1)
  4. 省份统计

    • map(find, parent)获取每个城市的最终根节点
    • set()去除重复根节点,其大小即为省份数量

示例

假设邻接矩阵为:

[[1,1,0],[1,1,0],[0,0,1]]

执行过程:

  1. 初始parent = [0,1,2]
  2. 处理(0,1):合并0和1,parent变为[0,0,2]
  3. (0,2)不相连跳过
  4. (1,2)不相连跳过
  5. 最终根节点:find(0)=0, find(1)=0, find(2)=2
  6. 不同根节点:{0,2},返回2

复杂度分析

  • 时间复杂度:O(n² α(n)),其中α是反阿克曼函数,通常视为常数
  • 空间复杂度:O(n),用于存储父节点数组

这种解法高效地解决了连通分量计数问题,是处理图连通性问题的经典方法。


6. 网络延迟时间(Dijkstra)

问题类型:单源最短路径 算法:Dijkstra(堆优化) LeetCode 743

def networkDelayTime(times, n, k):import heapq# 构建邻接表 (节点编号1到n)adj = [[] for _ in range(n+1)]  # adj[0]不使用for u, v, w in times:adj[u].append((v, w))  # 添加边u→v,权重w# 初始化优先队列和距离字典heap = [(0, k)]  # (距离, 节点),初始源节点距离为0dist = {i: float('inf') for i in range(1, n+1)}  # 初始距离设为无穷大dist[k] = 0  # 源节点到自身的距离为0# Dijkstra算法主循环while heap:d, u = heapq.heappop(heap)  # 取出当前距离最小的节点if d > dist[u]:  # 如果当前距离不是最短距离,跳过continue# 遍历u的所有邻居for v, w in adj[u]:if dist[v] > d + w:  # 发现更短路径dist[v] = d + w  # 更新距离heapq.heappush(heap, (dist[v], v))  # 加入优先队列# 计算最大延迟时间max_dist = max(dist.values())# 如果所有节点都可达则返回最大延迟,否则返回-1return max_dist if max_dist < float('inf') else -1

基本思路

  1. 构建图的邻接表表示
  2. 使用优先队列(最小堆)实现Dijkstra算法
  3. 维护距离字典记录从源节点到各节点的最短距离
  4. 返回所有最短距离中的最大值(即网络延迟时间)

关键点解析

  1. 邻接表构建

    • adj[u]存储从节点u出发的所有边及其权重
    • 节点编号假设为1到n(adj不使用)
  2. Dijkstra算法核心

    • 使用优先队列(最小堆)确保总是处理当前距离最近的节点
    • dist字典记录从源节点到各节点的最短距离
    • 当发现更短路径时更新距离并加入队列
  3. 延迟时间计算

    • 最大延迟时间即所有最短距离中的最大值
    • 如果存在不可达节点(距离仍为inf),返回-1
  4. 优化处理

    • if d > dist[u]: continue跳过已找到更优解的节点
    • 优先队列确保每个节点只被处理一次(最优情况下)

示例

假设输入:

  • times = [[2,1,1],[2,3,1],[3,4,1]]
  • n = 4
  • k = 2

执行过程:

  1. 构建邻接表:
    • adj = [(1,1), (3,1)]
    • adj = [(4,1)]
  2. 初始堆:[(0,2)],dist={1:∞, 2:0, 3:∞, 4:∞}
  3. 处理节点2:
    • 更新节点1距离为1,加入堆
    • 更新节点3距离为1,加入堆
  4. 处理节点1(距离1):无邻居
  5. 处理节点3(距离1):
    • 更新节点4距离为2,加入堆
  6. 处理节点4(距离2):无邻居
  7. 最终dist={1:1, 2:0, 3:1, 4:2}
  8. 最大延迟时间max_dist=2

复杂度分析

  • 时间复杂度:O(E log V),其中E是边数,V是节点数
  • 空间复杂度:O(V + E),用于存储邻接表和优先队列

Dijkstra算法是解决单源最短路径问题的经典算法,适用于非负权重的图。


7. 最小高度树(图中心)

问题类型:图的中心节点 算法:拓扑排序(剥洋葱法) LeetCode 310

def findMinHeightTrees(n, edges):if n == 1:  # 特殊情况处理:只有一个节点return [0]from collections import defaultdict# 构建邻接表(使用集合便于删除边)adj = defaultdict(set)for u, v in edges:adj[u].add(v)adj[v].add(u)# 初始化叶子节点列表(度为1的节点)leaves = [i for i in range(n) if len(adj[i]) == 1]# 核心算法:不断删除叶子节点while n > 2:n -= len(leaves)  # 更新剩余节点数new_leaves = []# 删除当前所有叶子节点for leaf in leaves:# 获取叶子节点的唯一邻居neighbor = adj[leaf].pop()# 从邻居的连接中删除该叶子节点adj[neighbor].remove(leaf)# 如果邻居变为新的叶子节点,加入下一轮处理if len(adj[neighbor]) == 1:new_leaves.append(neighbor)# 更新叶子节点列表leaves = new_leaves# 最后剩下的1或2个节点就是最小高度树的根return leaves

基本思路

  1. 构建图的邻接表表示
  2. 不断删除叶子节点(度为1的节点),直到剩下1或2个节点
  3. 最后剩下的节点就是最小高度树的根节点

关键点解析

  1. 邻接表构建

    • 使用defaultdict(set)存储每个节点的邻居
    • 便于快速添加和删除边
  2. 叶子节点删除过程

    • 每次迭代删除所有当前叶子节点
    • 检查被删除节点的邻居是否变为新叶子节点
    • 更新剩余节点计数
  3. 终止条件

    • 当剩余节点数n ≤ 2时停止
    • 树的最小高度根节点最多有2个
  4. 正确性保证

    • 长路径中间的节点最后被保留
    • 这些中间节点作为根时树的高度最小

示例

假设n=6,edges=[[0,1],[0,2],[0,3],[3,4],[3,5]]

执行过程:

  1. 初始叶子节点:[1,2,4,5]
  2. 第一轮:
    • 删除1,2,4,5
    • 0的邻居变为
    • 3的邻居变为
    • 新叶子节点:[0,3]
  3. 剩余节点n=2,停止
  4. 返回[0,3]

复杂度分析

  • 时间复杂度:O(V),其中V是节点数
  • 空间复杂度:O(V + E),用于存储邻接表

这种解法巧妙地利用了树的拓扑性质,通过层层剥离叶子节点来找到最优解,比暴力解法更高效。


8. 重新安排行程(欧拉路径)

问题类型:有向图欧拉路径 算法:Hierholzer算法 LeetCode 332

def findItinerary(tickets):from collections import defaultdict# 构建邻接表,并按字典序逆序排列graph = defaultdict(list)for u, v in sorted(tickets)[::-1]:  # 逆序排序graph[u].append(v)  # 这样pop()会按字典序取出route = []  # 存储最终路线def visit(node):# 递归访问所有相邻节点while graph[node]:  # 当节点还有未访问的边visit(graph[node].pop())  # 访问字典序最小的相邻节点route.append(node)  # 后序添加当前节点visit('JFK')  # 从JFK开始return route[::-1]  # 逆序后得到正确顺序

基本思路

  1. 构建图的邻接表,并按字典序逆序排列相邻节点
  2. 使用后序DFS遍历图
  3. 将访问的节点逆序输出即为正确行程

关键点解析

  1. 图的构建

    • 使用defaultdict(list)存储每个出发机场的目的地列表
    • sorted(tickets)[::-1]确保邻接表中的目的地按字典序逆序排列
    • 这样pop()操作会按字典序取出节点
  2. DFS遍历

    • visit函数递归处理当前节点的所有相邻节点
    • 使用while graph[node]确保处理完所有边
    • 采用后序遍历(先处理完所有子节点再添加当前节点)
  3. 结果处理

    • 递归完成后,route中是逆序的欧拉路径
    • 返回route[::-1]得到正确顺序

示例

假设输入:

tickets = [["JFK","SFO"],["JFK","ATL"],["SFO","ATL"],["ATL","JFK"],["ATL","SFO"]]

执行过程:

  1. 构建邻接表:
    • {'JFK': ['SFO', 'ATL'], 'SFO': ['ATL'], 'ATL': ['SFO', 'JFK']}
  2. 访问过程:
    • visit('JFK')
      • pop() 'SFO' → visit('SFO')
        • pop() 'ATL' → visit('ATL')
          • pop() 'SFO' → visit('SFO') → 无相邻节点 → 添加'SFO'
          • pop() 'JFK' → visit('JFK') → 已无相邻节点 → 添加'JFK'
        • 添加'ATL'
      • 添加'SFO'
      • pop() 'ATL' → visit('ATL') → 已无相邻节点 → 添加'ATL'
    • 添加'JFK'
  3. route = ['SFO', 'JFK', 'ATL', 'SFO', 'ATL', 'JFK']
  4. 返回逆序:['JFK', 'ATL', 'JFK', 'SFO', 'ATL', 'SFO']

复杂度分析

  • 时间复杂度:O(E log E),主要来自排序边
  • 空间复杂度:O(E),用于存储邻接表和递归栈

这种解法巧妙地利用了欧拉路径的性质和字典序要求,通过后序DFS和逆序输出得到正确结果。


9. 除法求值(加权图)

问题类型:加权图路径计算 算法:Floyd-Warshall/Union-Find LeetCode 399

def calcEquation(equations, values, queries):from collections import defaultdict# 构建图的邻接表graph = defaultdict(dict)# 添加已知的边关系for (u, v), w in zip(equations, values):graph[u][v] = w      # u/v = wgraph[v][u] = 1/w    # v/u = 1/w# Floyd-Warshall算法计算所有节点对的最优路径for k in graph:          # 中间节点for i in graph:      # 起始节点for j in graph:  # 目标节点# 如果i->k和k->j都有路径,则计算i->j的路径if k in graph[i] and j in graph[k]:graph[i][j] = graph[i][k] * graph[k][j]# 处理查询return [graph[u].get(v, -1.0) for u, v in queries]

基本思路

  1. 构建图的邻接表表示变量间的除法关系
  2. 使用Floyd-Warshall算法计算所有变量对之间的路径值
  3. 处理查询时直接从预计算的结果中查找

关键点解析

  1. 图的构建

    • 使用defaultdict(dict)构建邻接表
    • 每条边存储两个方向的权重(互为倒数)
    • 例如a/b=2,则存储graph[a][b]=2和graph[b][a]=0.5
  2. Floyd-Warshall算法

    • 三重循环:中间节点k、起始节点i、目标节点j
    • 如果i→k和k→j都有路径,则计算i→j的路径值为两者乘积
    • 最终graph[i][j]存储i/j的最优值
  3. 查询处理

    • 直接从预计算好的graph中查找结果
    • 如果变量不存在或没有路径,返回-1.0

示例

假设输入:

  • equations = [["a","b"],["b","c"]]
  • values = [2.0,3.0]
  • queries = [["a","c"],["b","a"],["a","e"],["a","a"],["x","x"]]

执行过程:

  1. 构建初始图:
    • {'a': {'b': 2.0}, 'b': {'a': 0.5, 'c': 3.0}, 'c': {'b': 0.333...}}
  2. Floyd-Warshall处理后:
    • 添加a→c = a→b * b→c = 2.0 * 3.0 = 6.0
    • 添加c→a = c→b * b→a ≈ 0.333 * 0.5 ≈ 0.166...
  3. 查询结果:
    • a/c → 6.0
    • b/a → 0.5
    • a/e → -1.0 (e不存在)
    • a/a → 1.0 (隐含在算法中)
    • x/x → -1.0 (x不存在)

复杂度分析

  • 时间复杂度:O(N³) + O(Q),其中N是变量数,Q是查询数
  • 空间复杂度:O(N²),用于存储邻接表

这种解法适用于变量关系可以表示为图的场景,通过预处理所有可能的路径,使得查询可以在常数时间内完成。


10. 水管网络(最大流)

问题类型:网络流问题 算法:Edmonds-Karp(BFS实现Ford-Fulkerson) LeetCode 变式

def maxFlow(graph, source, sink):from collections import deque# 初始化残差图residual = {u: {} for u in graph}  # 创建残差图的骨架for u in graph:for v, cap in graph[u]:  # 遍历原始图中的每条边residual[u][v] = cap  # 正向边初始为原始容量residual[v][u] = 0    # 反向边初始为0flow = 0  # 总流量初始化为0while True:  # 不断寻找增广路径# BFS寻找增广路径parent = {}  # 记录路径的父节点queue = deque([source])while queue:u = queue.popleft()for v in residual[u]:  # 遍历u的所有邻居# 如果v未被访问且还有剩余容量if v not in parent and residual[u][v] > 0:parent[v] = u  # 记录父节点if v == sink:  # 如果到达汇点,提前退出breakqueue.append(v)else:  # 没有找到到汇点的路径break  # 终止算法# 计算增广路径的瓶颈值path_flow = float('inf')v = sinkwhile v != source:  # 从汇点回溯到源点path_flow = min(path_flow, residual[parent[v]][v])v = parent[v]# 增加总流量flow += path_flow# 更新残差图v = sinkwhile v != source:u = parent[v]residual[u][v] -= path_flow  # 减少正向边容量residual[v][u] += path_flow  # 增加反向边容量v = ureturn flow  # 返回最大流值

基本思路

  1. 构建残差图(初始时与原始图相同)
  2. 在残差图中不断寻找增广路径
  3. 计算路径上的最小残差容量(瓶颈值)
  4. 更新残差图:正向边减少流量,反向边增加流量
  5. 当无法找到新的增广路径时,算法终止

关键点解析

  1. 残差图(residual graph)

    • 存储每条边的剩余容量
    • 包含原始图中不存在的反向边
    • 反向边的初始容量为0
  2. 增广路径查找

    • 使用BFS确保找到最短增广路径(Edmonds-Karp算法)
    • 只考虑剩余容量>0的边
    • 使用parent字典记录路径
  3. 流量更新

    • 找到路径上的最小剩余容量(path_flow)
    • 正向边减少path_flow
    • 反向边增加path_flow(允许算法"撤销"之前的流量分配)
  4. 算法终止条件

    • 当BFS无法找到从源点到汇点的路径时终止
    • 此时残差图中不再存在增广路径

示例

假设有下图:

    s/ \a   b\ / \c   d\ /t

边容量:

  • s→a:3, s→b:2
  • a→c:3, b→c:2, b→d:1
  • c→t:4, d→t:1

执行过程:

  1. 第一轮增广路径:s→a→c→t,流量3
  2. 第二轮增广路径:s→b→d→t,流量1
  3. 第三轮增广路径:s→b→c→t,流量1
  4. 无法再找到增广路径,算法终止
  5. 返回最大流5

复杂度分析

  • 时间复杂度:O(VE²),其中V是顶点数,E是边数
  • 空间复杂度:O(V²),用于存储残差图

这种实现是网络流问题的经典解法,广泛应用于运输问题、匹配问题、图像分割等多个领域。


问题分类总结

问题类型代表算法时间复杂度
连通性问题并查集/DFSO(α(n)) ~ O(V+E)
最短路径Dijkstra/Bellman-FordO((V+E)logV)
拓扑排序Kahn's/DFSO(V+E)
网络流Ford-FulkersonO(E * max_flow)
欧拉路径HierholzerO(E)

根据问题特征选择合适算法,小规模数据可用DFS/BFS简化实现,大规模分布式场景考虑GraphX/Giraph。

http://www.dtcms.com/a/343348.html

相关文章:

  • 面试可能问到的问题思考-Redis
  • 开源后台管理系统
  • 云蝠智能Voice Agent的多语言、多音色与语音克隆技术解析
  • 手动实现树形下拉菜单
  • 云原生俱乐部-RH294知识点归纳(2)
  • EEMD-LSTM模型择时策略 --- 1.EEMD分解与LSTM模型搭建
  • 开源,LangExtract-Python库用LLM从非结构化文本提取结构化信息
  • 生产环境的 MySQL 数据库能用 Docker 吗?
  • Spring面试宝典:Spring IOC的执行流程解析
  • ES_数据存储知识
  • 基于SpringBoot的宠物用品系统【2026最新】
  • odoo-063 pip 安装 Segmentation fault (core dumped),曲线救国
  • Vue3 + TypeScript全局阻止非输入区域的Backspace键,防止回退页面
  • Redis实战-基于Session实现分布式登录
  • 深度学习——yolo学习
  • AI模型部署 - 大语言模型(LLM)部署技术与框架
  • Android auncher3实现简单的负一屏功能
  • 基于YOLOv8-SEAttention与LLMs融合的农作物害虫智能诊断与防控决策系统
  • 运动数据采集如何帮助克里斯·凯尔飞跃迎面驶来的F1赛车
  • 基于IEEE-754浮点数格式的matlab仿真
  • Day24 目录遍历、双向链表、栈
  • Mac电脑 3D建模工具--犀牛Rhino
  • 【个人网络整理】NOIP / 省选 /NOI 知识点汇总
  • 视频孪生技术在城市政务数字化转型中的应用与价值探索
  • ES_映射
  • Nacos-10--认识Nacos中的Raft协议(Nacos强一致性的实现原理)
  • VirtualBox 安装 Ubuntu Server 系统及 Ubuntu 初始配置
  • 区块链联邦学习思路一
  • 14、软件实现与测试
  • 实践题:智能健康监测系统设计方案