图(Graph):关系网络的数学抽象

什么是图?

图是由顶点(Vertex/Node)边(Edge)组成的非线性数据结构,用于表示实体及其之间的关系。形式化定义为:

复制代码
G = (V, E)

其中V是顶点集合,E是边集合。

为什么图如此重要?

  1. 建模复杂关系:社交网络、交通系统、知识图谱
  2. 解决路径问题:导航、网络路由
  3. 依赖分析:任务调度、编译顺序
  4. 推荐系统:基于图神经网络
  5. 生物信息学:蛋白质相互作用网络

基本概念与术语

图的基本要素

  • 有向图/无向图:边是否有方向性
  • 加权图/无权图:边是否带有权重值
  • 连通图:任意两顶点间存在路径
  • 完全图:每对顶点间都有边连接
1. 有向图 vs 无向图
特征 有向图 (Directed Graph) 无向图 (Undirected Graph)
边的方向性 边有方向(A→B ≠ B→A) 边无方向(A-B = B-A)
表示方法 箭头表示方向 直线或曲线连接
邻接矩阵 非对称矩阵 对称矩阵
应用场景 网页链接、交通单行道、任务依赖 社交网络、地铁线路、分子结构
示例 A → B → C A --- B --- C
2. 加权图 vs 无权图
特征 加权图 (Weighted Graph) 无权图 (Unweighted Graph)
边的权重 边附带数值(如距离、成本) 所有边权重相同(默认为1)
路径计算 最短路径考虑权重和 最短路径=边数最少
算法差异 Dijkstra、Floyd-Warshall BFS、DFS
应用场景 地图导航、网络带宽分配 社交关系分析、迷宫求解
示例 A --5--> B --3--> C A --- B --- C
3. 连通图 vs 非连通图
特征 连通图 (Connected Graph) 非连通图 (Disconnected Graph)
定义 任意两顶点间存在路径 存在至少两个孤立子图
无向图要求 全图连通 有多个连通分量
有向图变体 强连通(双向路径)、弱连通(忽略方向后连通) 存在不可达顶点
应用场景 通信网络、电路板布线 岛屿分布、社交网络中的小群体
示例 A --- B --- C --- A(全连通) A --- BC --- D(两个子图)
4. 完全图 (Complete Graph)
特征 完全图
定义 每对不同的顶点之间都有一条边相连
边数公式 无向图:n(n-1)/2,有向图:n(n-1)(n为顶点数)
特点 边数最多、密度最高,任意两点直达
应用场景 理论证明、网络拓扑设计(如全连接服务器)
示例 3个顶点的无向完全图:A --- B --- C --- A(三角形)

如何快速区分?

  1. 看边是否有方向 → 有向/无向图
  2. 看边是否有数字 → 加权/无权图
  3. 任意两点能否互通 → 连通/非连通图
  4. 是否所有点都互相直连 → 是否完全图

经典算法应用场景

  • 有向加权图:Dijkstra找最短路径
  • 无向无权图:BFS找社交关系中的朋友
  • 非连通图:计算连通分量数量
  • 完全图:验证图论定理(如边数公式)

特殊图类型

  • :无环连通图(特殊的图)
  • 二分图:顶点可分为两个不相交集合
  • DAG(有向无环图):无循环的有向图
  • 欧拉图:包含欧拉回路的图
  • 哈密顿图:包含哈密顿回路的图
图类型 定义 关键性质 应用场景 示例
树 (Tree) 无环的连通无向图 边数 = 顶点数 - 1- 任意两点只有唯一路径 文件系统、决策树、网络拓扑 A --- B --- C (无环连通)
二分图 (Bipartite Graph) 顶点可分为两个不相交集合(U, V),所有边仅在U和V之间连接 当且仅当不含奇数长度的环- 可二着色(相邻节点颜色不同) 社交匹配、推荐系统 U: A, B; V: C, D边:A-C, B-D
DAG (有向无环图) 有方向且无环的图 可拓扑排序- 无自环和方向性环路 任务调度、依赖管理、版本控制 A → B → C (无C→A的环)
欧拉图 (Eulerian Graph) 包含欧拉回路(经过每边恰好一次且回到起点)的图 所有顶点度数为偶数(无向图) 入度=出度(有向图) 邮路问题、电路板布线 A --- B --- C --- D --- A (所有顶点度数为2)
哈密顿图 (Hamiltonian Graph) 包含哈密顿回路(经过每个顶点恰好一次且回到起点)的图 判定是NP完全问题- 充分条件(如Dirac定理)但无通用必要条件 旅行商问题、路径规划 A --- B --- C --- D --- A ( *** ** * ** *** )
核心区别总结
  1. 树 vs 普通图

    • 树是无环连通图的特殊情况,普通图可能有环或不连通。
  2. 二分图 vs 非二分图

    • 二分图可通过二着色测试验证(如用BFS),非二分图可能存在奇数环。
  3. DAG vs 有环有向图

    • DAG可进行拓扑排序 ,而有环图无法排序(如A→B→C→A)。
  4. 欧拉图 vs 哈密顿图

    • 欧拉图关注边遍历 (边不重复),哈密顿图关注顶点遍历(顶点不重复)。
  5. 欧拉图与哈密顿图的关系

    • 欧拉图不一定是哈密顿图,反之亦然。 示例
      • 菱形图(A-B-C-D-A加上A-C)是哈密顿图但不是欧拉图(存在奇度顶点)。
      • 四边形环(A-B-C-D-A)既是欧拉图也是哈密顿图。
算法与判定方法
图类型 判定方法 典型算法
- 边数 = 顶点数 - 1- DFS/BFS无环且连通 Kruskal、Prim(最小生成树)
二分图 BFS/DFS二着色法(相邻节点颜色需不同) 匈牙利算法(最大匹配)
DAG 拓扑排序(若排序失败则存在环) 动态规划(DAG上最长路径)
欧拉图 - 无向图:所有顶点度数为偶数- 有向图:入度=出度 Hierholzer算法(构造欧拉回路)
哈密顿图 - NP完全问题,无高效通用解法- 可用回溯法或启发式算法尝试 旅行商问题(TSP)的近似解法
实际应用举例
  1. :家族关系图谱、组织结构图。
  2. 二分图:求职平台(求职者↔岗位)、电影推荐(用户↔电影)。
  3. DAG:课程选修依赖关系、编译器的任务调度。
  4. 欧拉图:垃圾回收车的路线规划(每条街道清扫一次)。
  5. 哈密顿图:快递员的最短送货路线(每个地址访问一次)。

图的表示方法

1. 邻接矩阵

复制代码
# 无向图邻接矩阵示例
   A  B  C  D
A [0, 1, 1, 0]
B [1, 0, 1, 1]
C [1, 1, 0, 0]
D [0, 1, 0, 0]
  • 优点:快速判断两顶点是否相邻
  • 缺点:空间复杂度O(V²),稀疏图浪费空间

2. 邻接表

python 复制代码
{
    'A': ['B', 'C'],
    'B': ['A', 'C', 'D'],
    'C': ['A', 'B'],
    'D': ['B']
}
  • 优点:空间效率高O(V+E),适合稀疏图
  • 缺点:查询相邻关系稍慢

3. 边列表

python 复制代码
[('A','B'), ('A','C'), ('B','C'), ('B','D')]
  • 优点:简单直观,适合某些算法
  • 缺点:查询效率低

图的基本操作实现

Python图类实现(邻接表)

python 复制代码
class Graph:
    def __init__(self, directed=False):
        self.graph = {}  # 邻接表
        self.directed = directed
    
    def add_vertex(self, vertex):
        if vertex not in self.graph:
            self.graph[vertex] = []
    
    def add_edge(self, v1, v2, weight=None):
        self.add_vertex(v1)
        self.add_vertex(v2)
        self.graph[v1].append((v2, weight))
        if not self.directed:
            self.graph[v2].append((v1, weight))
    
    def get_vertices(self):
        return list(self.graph.keys())
    
    def get_edges(self):
        edges = []
        for vertex in self.graph:
            for neighbor, weight in self.graph[vertex]:
                if (vertex, neighbor, weight) not in edges:
                    edges.append((vertex, neighbor, weight))
        return edges

1. 类结构概述

python 复制代码
class Graph:
    def __init__(self, directed=False):
        self.graph = {}  # 邻接表
        self.directed = directed
  • graph :用字典实现邻接表,存储顶点和边。
    • 键(Key) :顶点(如 'A'
    • 值(Value) :邻接顶点列表,元素为元组 (邻居, 权重)(如 [('B', 3), ('C', 5)]
  • directed:标记是否为有向图(默认无向)。

2. 方法解析与图示

(1)添加顶点 add_vertex
python 复制代码
def add_vertex(self, vertex):
    if vertex not in self.graph:
        self.graph[vertex] = []  # 初始化空邻接列表

操作示例

python 复制代码
g = Graph()
g.add_vertex('A')
g.add_vertex('B')

内存结构

python 复制代码
{
    'A': [],  # 顶点A暂无邻居
    'B': []   # 顶点B暂无邻居
}
(2)添加边 add_edge
python 复制代码
def add_edge(self, v1, v2, weight=None):
    self.add_vertex(v1)
    self.add_vertex(v2)
    self.graph[v1].append((v2, weight))  # v1→v2
    if not self.directed:                # 无向图需双向添加
        self.graph[v2].append((v1, weight))

操作示例

python 复制代码
g = Graph(directed=False)
g.add_edge('A', 'B', weight=3)
g.add_edge('B', 'C', weight=2)

内存结构

python 复制代码
{
    'A': [('B', 3)],  # A ↔ B
    'B': [('A', 3), ('C', 2)],
    'C': [('B', 2)]
}

图示

python 复制代码
     3
  A ----- B
         /
      2 /
       C
(3)获取顶点 get_vertices
python 复制代码
def get_vertices(self):
    return list(self.graph.keys())  # 返回所有顶点

示例输出

python 复制代码
['A', 'B', 'C']
(4)获取边 get_edges
python 复制代码
def get_edges(self):
    edges = []
    for vertex in self.graph:
        for neighbor, weight in self.graph[vertex]:
            edges.append((vertex, neighbor, weight))
    return edges

示例输出

python 复制代码
[('A', 'B', 3), ('B', 'A', 3), ('B', 'C', 2), ('C', 'B', 2)]

3. 有向图 vs 无向图对比

无向图示例
python 复制代码
g = Graph(directed=False)
g.add_edge('X', 'Y', 1)

内存结构

python 复制代码
{
    'X': [('Y', 1)],
    'Y': [('X', 1)]  # 自动反向添加
}

图示

python 复制代码
  X --------- Y
有向图示例
python 复制代码
g = Graph(directed=True)
g.add_edge('X', 'Y', 1)

内存结构

python 复制代码
{
    'X': [('Y', 1)],
    'Y': []  # 无反向边
}

图示

python 复制代码
  X ---→ Y

4. 完整测试案例

python 复制代码
# 创建无向加权图
g = Graph(directed=False)
g.add_edge('A', 'B', 3)
g.add_edge('B', 'C', 2)
g.add_edge('A', 'C', 5)

# 输出结果
print("顶点:", g.get_vertices())  # ['A', 'B', 'C']
print("边:", g.get_edges())      # [('A','B',3), ('B','A',3), ('B','C',2), ...]

# 可视化结构
"""
     3
  A ----- B
  |     /
5 |   / 2
  | /
  C
"""

5. 关键点总结

  1. 邻接表:用字典高效存储顶点和边。
  2. 权重支持 :边的权重通过元组 (邻居, 权重) 存储。
  3. 方向控制directed 参数决定是否自动添加反向边。
  4. 动态扩展:添加边时会自动创建不存在的顶点。

图的遍历算法

深度优先搜索(DFS)

python 复制代码
def dfs(graph, start, visited=None):
    if visited is None:
        visited = set()          # 初始化已访问集合
    visited.add(start)           # 标记当前节点为已访问
    print(start, end=' ')        # 输出访问的节点
    for neighbor, _ in graph[start]:  # 遍历邻居(忽略权重)
        if neighbor not in visited:
            dfs(graph, neighbor, visited)  # 递归访问未访问的邻居

1. 核心步骤图示

假设有以下无向图(邻接表表示):

python 复制代码
graph = {
    'A': [('B', 1), ('C', 1)],
    'B': [('A', 1), ('D', 1), ('E', 1)],
    'C': [('A', 1), ('F', 1)],
    'D': [('B', 1)],
    'E': [('B', 1), ('F', 1)],
    'F': [('C', 1), ('E', 1)]
}

图示

python 复制代码
      A
    /   \
   B     C
  / \   /
 D   E-F

2. 执行过程分步演示

从起点 'A' 开始 DFS 的递归流程:

递归层 当前节点 已访问集合 输出顺序 动作说明
1 A {'A'} A 访问 A,递归访问 B
2 B {'A', 'B'} A B 访问 B,递归访问 D
3 D {'A', 'B', 'D'} A B D D 无未访问邻居,回溯到 B
2 B {'A', 'B', 'D'} - 访问 B 的下一个邻居 E
3 E {'A', 'B', 'D', 'E'} A B D E 访问 E,递归访问 F
4 F {'A', 'B', 'D', 'E', 'F'} A B D E F 访问 F,递归访问 C
5 C {'A', 'B', 'D', 'E', 'F', 'C'} A B D E F C C 已无未访问邻居,回溯到 F
4 F {'A', 'B', 'D', 'E', 'F', 'C'} - F 的邻居均已访问,回溯到 E
... ... ... ... 最终输出:A B D E F C

3. 关键点说明

  1. 递归终止条件:当节点的所有邻居均被访问时,递归终止。
  2. 回溯机制 :通过递归调用栈自动实现回溯(如访问完 D 后回到 B)。
  3. 避免重复访问visited 集合确保每个节点只处理一次。
  4. 忽略权重 :代码中 for neighbor, _ in graph[start]_ 表示忽略权重。

4. 输出结果

执行 dfs(graph, 'A') 的输出结果为: A B D E F C

5. 对比 BFS 的访问顺序

若使用广度优先搜索(BFS),同一图的访问顺序为: A B C D E F (使用队列按层遍历,与 DFS 的深入优先不同)

6. 常见问题解答

Q1: 如何处理有向图?

  • 代码无需修改!邻接表已隐含方向性(例如 'A': [('B',1)] 表示 A→B)。

Q2: 如何记录访问路径而非仅输出节点?

  • 修改代码为传递路径列表:
python 复制代码
  def dfs_path(graph, start, path=None, visited=None):
      if visited is None:
          visited = set()
      if path is None:
          path = []
      visited.add(start)
      path.append(start)
      for neighbor, _ in graph[start]:
          if neighbor not in visited:
              dfs_path(graph, neighbor, path, visited)
      return path

输出['A', 'B', 'D', 'E', 'F', 'C']

Q3: 非递归实现如何写?

  • 使用栈模拟递归:
python 复制代码
  def dfs_iterative(graph, start):
      visited = set()
      stack = [start]
      while stack:
          node = stack.pop()
          if node not in visited:
              print(node, end=' ')
              visited.add(node)
              for neighbor, _ in reversed(graph[node]):  # 保持顺序一致
                  if neighbor not in visited:
                      stack.append(neighbor)

广度优先搜索(BFS)

python 复制代码
from collections import deque  # 使用双端队列实现高效队列

def bfs(graph, start):
    visited = set()            # 记录已访问节点
    queue = deque([start])     # 初始化队列,从起点开始
    visited.add(start)         # 标记起点为已访问
    
    while queue:               # 当队列不为空时循环
        vertex = queue.popleft()  # 取出队列左侧节点(先进先出)
        print(vertex, end=' ') # 输出当前节点
        for neighbor, _ in graph[vertex]:  # 遍历当前节点的邻居(忽略权重)
            if neighbor not in visited:
                visited.add(neighbor)      # 标记邻居为已访问
                queue.append(neighbor)     # 将邻居加入队列

1. 核心步骤图示

假设有以下无向图(邻接表表示):

复制代码
graph = {
    'A': [('B', 1), ('C', 1)],
    'B': [('A', 1), ('D', 1), ('E', 1)],
    'C': [('A', 1), ('F', 1)],
    'D': [('B', 1)],
    'E': [('B', 1), ('F', 1)],
    'F': [('C', 1), ('E', 1)]
}

图示

复制代码
      A
    /   \
   B     C
  / \   /
 D   E-F

2. 执行过程分步演示

从起点 'A' 开始 BFS 的流程:

队列状态 当前节点 已访问集合 输出顺序 动作说明
[A] A {'A'} A 访问 A,将 B、C 入队
[B, C] B {'A', 'B'} A B 访问 B,将 D、E 入队
[C, D, E] C {'A', 'B', 'C'} A B C 访问 C,将 F 入队
[D, E, F] D {'A', 'B', 'C', 'D'} A B C D 访问 D,无新邻居入队
[E, F] E {'A', 'B', 'C', 'D', 'E'} A B C D E 访问 E,F 已入队,跳过
[F] F {'A', 'B', 'C', 'D', 'E', 'F'} A B C D E F 访问 F,队列空,结束

3. 关键点说明

  1. 队列的作用 :确保按"先进先出"顺序访问节点,实现层级遍历。
  2. visited 集合 :防止重复访问(如 A-B-E-F-C 中 F 已被 E 加入队列,C 访问时不重复处理)。
  3. 权重忽略for neighbor, _ in graph[vertex] 中的 _ 表示忽略权重。
  4. 与 DFS 的区别
    • BFS 用队列,按层扩展;DFS 用栈/递归,深度优先。
    • 上例中 BFS 输出 A B C D E F,而 DFS 输出 A B D E F C

4. 输出结果

执行 bfs(graph, 'A') 的输出结果为: A B C D E F

5. 应用场景

  • 最短路径(无权图):BFS 天然保证找到的路径是最短的。
  • 社交网络好友推荐:按距离层级推荐朋友。
  • 迷宫最短路径:从起点逐层探索可达位置。

6. 常见问题解答

Q1: 如何记录节点的层级? 修改代码,在入队时保存层级:

复制代码
def bfs_levels(graph, start):
    visited = {start: 0}  # 记录节点及其层级
    queue = deque([(start, 0)])  # (节点, 层级)
    while queue:
        vertex, level = queue.popleft()
        print(f"{vertex} (Level {level})")
        for neighbor, _ in graph[vertex]:
            if neighbor not in visited:
                visited[neighbor] = level + 1
                queue.append((neighbor, level + 1))

输出

复制代码
A (Level 0)
B (Level 1)
C (Level 1)
D (Level 2)
E (Level 2)
F (Level 2)

Q2: 如何实现有向图的 BFS? 代码无需修改!邻接表已隐含方向性(如 'A': [('B',1)] 表示 A→B)。

Q3: 非连通图如何处理? 对所有未访问的节点依次调用 BFS:

复制代码
def bfs_all_components(graph):
    visited = set()
    for node in graph:
        if node not in visited:
            bfs(graph, node, visited)  # 需修改原函数支持传入visited

经典图算法

1. 最短路径

Dijkstra算法(非负权图)
python 复制代码
import heapq

def dijkstra(graph, start):
    # 初始化所有节点距离为无穷大
    distances = {v: float('inf') for v in graph}
    distances[start] = 0  # 起点到自身的距离为0
    
    # 使用最小堆优先处理当前最短路径
    heap = [(0, start)]
    
    while heap:
        current_dist, current = heapq.heappop(heap)
        # 如果当前距离大于已知最短距离,跳过
        if current_dist > distances[current]:
            continue
        # 遍历邻居节点
        for neighbor, weight in graph[current]:
            distance = current_dist + weight
            # 发现更短路径时更新
            if distance < distances[neighbor]:
                distances[neighbor] = distance
                heapq.heappush(heap, (distance, neighbor))
    return distances

1. 核心步骤图示

假设有以下有向加权图(邻接表表示):

python 复制代码
graph = {
    'A': [('B', 1), ('C', 4)],
    'B': [('C', 2), ('D', 5)],
    'C': [('D', 1)],
    'D': []
}
图示
python 复制代码
       A
    1/   \4
    B     C
  2/ \1  / 
  D   E

2. 执行过程分步演示

从起点 'A' 计算最短路径:

堆状态 当前节点 已知最短距离 动作说明
[(0, A)] A A:0, B:∞, C:∞, D:∞ 处理A,将B(1)、C(4)加入堆
[(1, B), (4, C)] B A:0, B:1, C:3, D:6 发现B→C更短路径(1+2=3),更新C
[(3, C), (4, C)] C A:0, B:1, C:3, D:4 发现C→D更短路径(3+1=4),更新D
[(4, D)] D A:0, B:1, C:3, D:4 D无邻居,结束

最终结果

python 复制代码
{'A': 0, 'B': 1, 'C': 3, 'D': 4}

3. 关键点说明

  1. 最小堆的作用:始终优先处理当前距离起点最近的节点(贪心策略)。
  2. 距离更新条件:仅当找到更短路径时更新并加入堆。
  3. 跳过无效记录if current_dist > distances[current]: continue 避免重复处理。
  4. 时间复杂度 :O((V+E) log V),V为顶点数,E为边数。

4. 与 BFS 的区别

特性 Dijkstra BFS
边权重 处理加权图 仅适用于无权图
数据结构 最小堆 队列
目标 计算最短路径(权重和最小) 计算最少边数路径
复杂度 O((V+E) log V) O(V+E)

5. 应用场景

  • 地图导航:计算两地间的最短行驶距离。
  • 网络路由:寻找数据传输的最优路径。
  • 任务调度:优化资源分配路径。

6. 常见问题解答

Q1: 如何处理负权边? Dijkstra 无法处理负权边(可能导致无限循环),需改用 Bellman-Ford 算法

Q2: 如何记录完整路径? 添加 previous 字典记录前驱节点:

python 复制代码
def dijkstra_with_path(graph, start):
    distances = {v: float('inf') for v in graph}
    previous = {v: None for v in graph}  # 新增
    distances[start] = 0
    heap = [(0, start)]
    
    while heap:
        current_dist, current = heapq.heappop(heap)
        if current_dist > distances[current]:
            continue
        for neighbor, weight in graph[current]:
            distance = current_dist + weight
            if distance < distances[neighbor]:
                distances[neighbor] = distance
                previous[neighbor] = current  # 记录前驱节点
                heapq.heappush(heap, (distance, neighbor))
    return distances, previous

示例输出

python 复制代码
distances = {'A':0, 'B':1, 'C':3, 'D':4}
previous = {'A':None, 'B':'A', 'C':'B', 'D':'C'}
# 从D回溯路径:D→C→B→A
Q3: 为什么用堆而不用普通队列? 堆能高效获取当前最小距离节点,确保每次处理的都是全局最优解。

Floyd-Warshall算法(所有顶点对)
python 复制代码
def floyd_warshall(graph):
    vertices = list(graph.keys())  # 获取所有顶点
    n = len(vertices)
    # 初始化距离矩阵(n x n的二维数组)
    dist = [[float('inf')] * n for _ in range(n)]
    
    # 1. 初始化距离矩阵
    for i in range(n):
        dist[i][i] = 0  # 顶点到自身的距离为0
        for neighbor, weight in graph[vertices[i]]:
            j = vertices.index(neighbor)  # 找到邻居的索引
            dist[i][j] = weight  # 直接边的权重
    
    # 2. 动态规划求解所有顶点对的最短路径
    for k in range(n):          # 中间顶点
        for i in range(n):      # 起始顶点
            for j in range(n):  # 目标顶点
                # 如果通过k的路径更短,则更新
                if dist[i][j] > dist[i][k] + dist[k][j]:
                    dist[i][j] = dist[i][k] + dist[k][j]
    
    # 3. 转换为字典形式返回结果
    return {v: dict(zip(vertices, row)) for v, row in zip(vertices, dist)}

1. 核心步骤图示

假设有以下有向加权图(允许负权边,但不含负权环):

python 复制代码
graph = {
    'A': [('B', 3), ('D', 7)],
    'B': [('A', 8), ('C', 2)],
    'C': [('D', 1), ('A', 5)],
    'D': [('B', 2)]
}
图示
python 复制代码
       A
   3↗↙8  ↘7
    B     D
  2↘  ↖2 ↗1
      C

2. 执行过程分步演示

初始化距离矩阵
A B C D
A 0 3 7
B 8 0 2
C 5 0 1
D 2 0
动态规划过程(k为中间节点)
  • k=0(A)
    • 更新B→D:dist[B][D] = min(∞, 8+7) = ∞(无变化)
    • 更新C→B:dist[C][B] = min(∞, 5+3) = 8
  • k=1(B)
    • 更新A→C:dist[A][C] = min(∞, 3+2) = 5
    • 更新D→A:dist[D][A] = min(∞, 2+8) = 10
  • k=2(C)
    • 更新A→D:dist[A][D] = min(7, 5+1) = 6
    • 更新B→A:dist[B][A] = min(8, 2+5) = 7
  • k=3(D)
    • 更新A→B:dist[A][B] = min(3, 6+2) = 3(无变化)
    • 更新C→B:dist[C][B] = min(8, 1+2) = 3
最终距离矩阵
A B C D
A 0 3 5 6
B 7 0 2 3
C 5 3 0 1
D 9 2 4 0

返回结果

python 复制代码
{
    'A': {'A':0, 'B':3, 'C':5, 'D':6},
    'B': {'A':7, 'B':0, 'C':2, 'D':3},
    'C': {'A':5, 'B':3, 'C':0, 'D':1},
    'D': {'A':9, 'B':2, 'C':4, 'D':0}
}

3. 关键点说明

  1. 动态规划思想 :通过中间节点 k 逐步优化所有顶点对 (i,j) 的路径。
  2. 负权边处理:允许负权边,但图中不能有负权环(否则最短路径无意义)。
  3. 时间复杂度:O(V³),适合稠密图或顶点数较少的情况。
  4. 空间优化:可原地修改矩阵,但代码中保留了清晰的中间状态。

4. 与 Dijkstra 的对比

特性 Floyd-Warshall Dijkstra
适用场景 所有顶点对的最短路径 单源最短路径
边权重 允许负权(无负权环) 仅非负权
时间复杂度 O(V³) O((V+E) log V)
输出结果 全部顶点对的距离矩阵 单源到其他顶点的距离

5. 应用场景

  • 网络路由表:计算所有路由器之间的最短路径。
  • 交通规划:城市间最短路径的全局预计算。
  • 游戏地图寻路:预先生成所有位置之间的移动成本。

6. 常见问题解答

Q1: 如何检测负权环? 运行算法后,检查对角线元素 dist[i][i]:若存在 dist[i][i] < 0,则说明有负权环。

Q2: 如何重建具体路径? 额外维护 next 矩阵记录路径:

python 复制代码
next = [[None]*n for _ in range(n)]
# 初始化时:
if i != j and dist[i][j] != float('inf'):
    next[i][j] = j
# 更新时:
if dist[i][j] > dist[i][k] + dist[k][j]:
    dist[i][j] = dist[i][k] + dist[k][j]
    next[i][j] = next[i][k]  # 记录中间节点

# 重建路径函数
def get_path(i, j):
    if next[i][j] is None:
        return []
    path = [i]
    while i != j:
        i = next[i][j]
        path.append(i)
    return path

Q3: 为什么用 inf 初始化? inf 表示初始时顶点之间不可达,后续通过动态规划逐步更新为实际最短距离。


2. 最小生成树(MST)

Prim算法
python 复制代码
import heapq

def prim(graph):
    if not graph:
        return []  # 空图直接返回
    
    mst = []               # 存储最小生成树的边
    visited = set()        # 记录已访问的顶点
    start = next(iter(graph))  # 随机选择一个起始顶点
    visited.add(start)
    
    # 初始化堆,存储起始顶点的所有边
    edges = [
        (weight, start, neighbor)
        for neighbor, weight in graph[start]
    ]
    heapq.heapify(edges)  # 转换为最小堆
    
    while edges:
        weight, u, v = heapq.heappop(edges)  # 取出当前最小权重的边
        if v not in visited:
            visited.add(v)
            mst.append((u, v, weight))  # 加入最小生成树
            # 将新顶点的未访问邻居边加入堆
            for neighbor, weight in graph[v]:
                if neighbor not in visited:
                    heapq.heappush(edges, (weight, v, neighbor))
    return mst

1. 核心步骤图示

假设有以下无向加权图(邻接表表示):

python 复制代码
graph = {
    'A': [('B', 2), ('D', 6)],
    'B': [('A', 2), ('C', 3), ('D', 8), ('E', 5)],
    'C': [('B', 3), ('E', 7)],
    'D': [('A', 6), ('B', 8), ('E', 9)],
    'E': [('B', 5), ('C', 7), ('D', 9)]
}
图示
python 复制代码
        A
    2↙   ↘6
    B     D
 3↙ \5 8↙ ↘9
 C   E
  7↙

2. 执行过程分步演示

从顶点 'A' 开始构建 MST:

堆状态 (weight,u,v) 已访问顶点 MST 边 动作说明
[(2,A,B), (6,A,D)] {'A'} [] 初始将A的边加入堆
[(6,A,D)] {'A', 'B'} [('A', 'B', 2)] 选择最小边 A-B,将B的边加入堆
[(3,B,C), (5,B,E), (6,A,D), (8,B,D)] {'A', 'B'} [('A', 'B', 2)] 堆更新
[(5,B,E), (6,A,D), (8,B,D)] {'A', 'B', 'C'} [('A','B',2), ('B','C',3)] 选择 B-C,将C的边加入堆
[(5,B,E), (6,A,D), (7,C,E), (8,B,D)] {'A','B','C'} - 堆更新
[(6,A,D), (7,C,E), (8,B,D)] {'A','B','C','E'} [..., ('B','E',5)] 选择 B-E,将E的边加入堆
[(6,A,D), (7,C,E), (8,B,D), (9,E,D)] {'A','B','C','E'} - 堆更新
[(7,C,E), (8,B,D), (9,E,D)] {'A','B','C','E','D'} [..., ('A','D',6)] 选择 A-D,结束(所有顶点已访问)

最终 MST 边[('A', 'B', 2), ('B', 'C', 3), ('B', 'E', 5), ('A', 'D', 6)] 总权重:2 + 3 + 5 + 6 = 16

3. 关键点说明

  1. 贪心策略:每次选择当前连接已访问和未访问顶点的最小权重边。
  2. 堆的作用:高效获取当前最小权重边(时间复杂度 O(log E))。
  3. 避免成环 :仅处理未访问顶点的边(通过 visited 集合保证)。
  4. 时间复杂度:O(E log E),适合稠密图。

4. 与 Kruskal 算法的对比

特性 Prim Kruskal
数据结构 最小堆 + 邻接表 并查集 + 排序所有边
适用图类型 稠密图(邻接表更高效) 稀疏图(排序边成本低)
时间复杂度 O(E log E) O(E log V)
空间复杂度 O(V + E) O(E)

5. 应用场景

  • 网络设计:构建成本最低的通信网络。
  • 电路板布线:最小化导线总长度。
  • 管道系统:优化水/电管道的连接路径。

6. 常见问题解答

Q1: 如何处理非连通图? Prim 算法只能得到连通分量的 MST。若图不连通,需对每个连通分量分别运行 Prim。

Q2: 如何验证结果的正确性? 检查以下条件:

  1. 边数 = 顶点数 - 1。
  2. 总权重与 Kruskal 算法结果一致。
  3. 无环且连接所有顶点。

Q3: 为什么不用优先队列而用堆? Python 的 heapq 模块实现的是最小堆,本质上就是优先队列的高效实现。


Kruskal算法
python 复制代码
def kruskal(graph):
    parent = {}  # 并查集数据结构
    
    # 查找根节点(路径压缩优化)
    def find(u):
        while parent[u] != u:
            parent[u] = parent[parent[u]]  # 路径压缩
            u = parent[u]
        return u
    
    # 合并两个集合
    def union(u, v):
        u_root = find(u)
        v_root = find(v)
        if u_root == v_root:
            return False  # 已在同一集合,无需合并
        parent[v_root] = u_root
        return True
    
    # 1. 初始化所有边并按权重排序
    edges = []
    for u in graph:
        parent[u] = u  # 每个节点初始时自成一集合
        for v, weight in graph[u]:
            edges.append((weight, u, v))
    edges.sort()  # 按权重升序排序
    
    # 2. 遍历所有边,构建MST
    mst = []
    for weight, u, v in edges:
        if union(u, v):  # 如果边不会形成环
            mst.append((u, v, weight))
    return mst

1. 核心步骤图示

假设有以下无向加权图(邻接表表示):

python 复制代码
graph = {
    'A': [('B', 2), ('D', 6)],
    'B': [('A', 2), ('C', 3), ('D', 8), ('E', 5)],
    'C': [('B', 3), ('E', 7)],
    'D': [('A', 6), ('B', 8), ('E', 9)],
    'E': [('B', 5), ('C', 7), ('D', 9)]
}
图示

2. 执行过程分步演示

步骤1:边排序

所有边按权重升序排序:

python 复制代码
(2,A,B), (3,B,C), (5,B,E), (6,A,D), (7,C,E), (8,B,D), (9,D,E)
步骤2:构建MST
当前边 操作 并查集状态 MST 边
(A,B,2) 合并A和B {A:A, B:A, C:C, D:D, E:E} [('A','B',2)]
(B,C,3) 合并B(A)和C {A:A, B:A, C:A, D:D, E:E} [..., ('B','C',3)]
(B,E,5) 合并B(A)和E {A:A, B:A, C:A, D:D, E:A} [..., ('B','E',5)]
(A,D,6) 合并A和D {A:A, B:A, C:A, D:A, E:A} [..., ('A','D',6)]
(C,E,7) C和E已在同一集合(跳过) - 无变化
(B,D,8) B和D已在同一集合(跳过) - 无变化
(D,E,9) D和E已在同一集合(跳过) - 无变化

最终 MST 边[('A', 'B', 2), ('B', 'C', 3), ('B', 'E', 5), ('A', 'D', 6)] 总权重:2 + 3 + 5 + 6 = 16

3. 关键点说明

  1. 并查集优化
    • find 函数使用路径压缩,降低后续查找时间复杂度。
    • union 函数通过比较根节点避免环的形成。
  2. 贪心策略:按权重升序选择边,确保每次加入的边是当前最小的有效边。
  3. 时间复杂度
    • 排序:O(E log E)
    • 并查集操作:O(α(V))(近似常数)
    • 总复杂度:O(E log E)(适合稀疏图)

4. 与 Prim 算法的对比

特性 Kruskal Prim
数据结构 并查集 + 排序边 最小堆 + 邻接表
适用图类型 稀疏图(边少) 稠密图(边多)
边处理顺序 全局排序后选择 从当前顶点逐步扩展
是否需要连通图 是(否则得到最小生成森林)

5. 应用场景

  • 电网设计:用最小成本连接所有城市。
  • 道路规划:建设总长度最短的公路网。
  • 聚类分析:基于距离合并相似数据点。

6. 常见问题解答

Q1: 如何验证结果的正确性? 检查:

  1. 边数 = 顶点数 - 1
  2. 总权重与 Prim 算法结果一致
  3. 通过并查集确认无环

Q2: 如何处理非连通图? 算法会返回最小生成森林(各连通分量的 MST),可通过检查 parent 中根节点的数量判断连通性。

Q3: 为什么边排序用 sort() 而不是堆?

  • 排序后只需一次遍历(O(E)),而堆需要多次弹出(O(E log E))。
  • 实际时间复杂度相同,但排序代码更简洁。

3. 拓扑排序(DAG)

python 复制代码
from collections import deque

def topological_sort(graph):
    # 1. 计算所有顶点的入度
    in_degree = {u: 0 for u in graph}
    for u in graph:
        for v, _ in graph[u]:  # 忽略权重
            in_degree[v] += 1
    
    # 2. 初始化队列(入度为0的顶点)
    queue = deque([u for u in in_degree if in_degree[u] == 0])
    topo_order = []
    
    # 3. 不断移除入度为0的顶点
    while queue:
        u = queue.popleft()
        topo_order.append(u)
        for v, _ in graph[u]:
            in_degree[v] -= 1  # 删除边u→v
            if in_degree[v] == 0:
                queue.append(v)
    
    # 4. 检查是否存在环
    if len(topo_order) == len(graph):
        return topo_order  # 有效拓扑序
    else:
        return []  # 存在环,无法拓扑排序

1. 核心步骤图示

假设有以下有向无环图(DAG)(邻接表表示):

python 复制代码
graph = {
    'A': [('B', 1), ('C', 1)],  # A → B, A → C
    'B': [('D', 1)],             # B → D
    'C': [('D', 1), ('E', 1)],   # C → D, C → E
    'D': [('F', 1)],             # D → F
    'E': [('F', 1)],             # E → F
    'F': []                      # F 无出边
}

图示

python 复制代码
      A
    /   \
   B     C
    \  /  \
     D     E
      \   /
        F

2. 执行过程分步演示

步骤1:计算入度
顶点 A B C D E F
入度 0 1 1 2 1 2
步骤2:初始化队列

queue = deque(['A']) (只有A的入度为0)

步骤3:处理队列
操作 队列状态 拓扑序 入度更新
处理A,移除A [] [A] B:0, C:0
将B、C加入队列 [B, C] - -
处理B,移除B [C] [A, B] D:1
处理C,移除C [] [A, B, C] D:0, E:0
将D、E加入队列 [D, E] - -
处理D,移除D [E] [A,B,C,D] F:1
处理E,移除E [] [A,B,C,D,E] F:0
将F加入队列 [F] - -
处理F,移除F [] [A,B,C,D,E,F] -

最终拓扑序['A', 'B', 'C', 'D', 'E', 'F'] (其他可能的拓扑序:['A', 'C', 'E', 'B', 'D', 'F']

3. 关键点说明

  1. 入度(In-degree):指向顶点的边数量,用于确定可处理的顶点。
  2. 队列选择:优先处理入度为0的顶点(保证无前驱依赖)。
  3. 环检测:若结果列表长度 ≠ 顶点数,说明存在环。
  4. 时间复杂度:O(V + E),适合大规模DAG。

4. 应用场景

  • 任务调度:编译器的依赖管理(如Makefile)。
  • 课程安排:选修课的先后顺序规划。
  • 工作流引擎:步骤的依赖关系处理。

5. 常见问题解答

Q1: 如何处理非DAG图? 算法会返回空列表(如检测到环),例如:

python 复制代码
graph = {'A': [('B',1)], 'B': [('A',1)]}  # A↔B
print(topological_sort(graph))  # [](存在环)

Q2: 如何获取所有可能的拓扑序? 需用回溯法枚举所有可能的顺序,但时间复杂度较高(O(V!))。

Q3: 为什么用队列而不用栈? 队列保证BFS顺序(广度优先),栈实现DFS顺序(深度优先)。两者均可得到有效拓扑序,但顺序不同。

6. 扩展:动态图中维护拓扑序

对于频繁增删边的图,可使用增量式拓扑排序算法(如Kahn算法的动态版本)。


图的高级应用

1. 连通分量检测

python 复制代码
def connected_components(graph):
    visited = set()                     # 记录已访问顶点
    components = []                     # 存储所有连通分量
    
    for vertex in graph:                # 遍历每个顶点
        if vertex not in visited:
            # 初始化DFS栈
            stack = [vertex]
            visited.add(vertex)
            component = []              # 当前连通分量
            
            while stack:
                node = stack.pop()      # 弹出栈顶顶点
                component.append(node)
                # 遍历邻居
                for neighbor, _ in graph[node]:  # 忽略权重
                    if neighbor not in visited:
                        visited.add(neighbor)
                        stack.append(neighbor)
            
            components.append(component)
    return components

1. 核心步骤图示

假设有以下无向图(邻接表表示,权重忽略):

python 复制代码
graph = {
    'A': [('B', 1), ('C', 1)],
    'B': [('A', 1), ('C', 1)],
    'C': [('A', 1), ('B', 1)],
    'D': [('E', 1)],
    'E': [('D', 1)],
    'F': []
}
图示
python 复制代码
      A
    /   \
   B     C    D --- E   F(孤立)

2. 执行过程分步演示

步骤1:初始化
  • visited = {}
  • components = []
步骤2:处理顶点A
  • 发现A未访问,启动DFS:
    • 栈初始化:stack = ['A']
    • 访问A的邻居B、C:
      • 将B、C加入栈和visited
    • 弹出C,访问其邻居A、B(A已访问):
      • 无新节点加入
    • 弹出B,访问其邻居A、C(均已访问)
  • 当前连通分量:['A', 'C', 'B']
步骤3:处理顶点D
  • 发现D未访问,启动DFS:
    • 栈初始化:stack = ['D']
    • 访问D的邻居E:
      • 将E加入栈和visited
    • 弹出E,访问其邻居D(已访问)
  • 当前连通分量:['D', 'E']
步骤4:处理顶点F
  • 发现F未访问且无邻居:
  • 当前连通分量:['F']
最终结果
python 复制代码
[ ['A', 'C', 'B'], ['D', 'E'], ['F'] ]

3. 关键点说明

  1. DFS/BFS选择:代码使用DFS(栈实现),也可改用BFS(队列实现)。
  2. 无向图处理:算法隐式处理无向边(如A-B和B-A视为同一条边)。
  3. 时间复杂度:O(V + E),每个顶点和边仅处理一次。
  4. 权重忽略for neighbor, _ in graph[node] 中的 _ 表示忽略权重。

4. 与强连通分量(SCC)的区别

特性 连通分量(无向图) 强连通分量(有向图)
定义 顶点间双向可达的子图 顶点间双向有向路径的子图
算法 DFS/BFS Kosaraju或Tarjan算法
示例 A-B-C 是一个连通分量 A→B→C→A 是一个强连通分量

5. 应用场景

  • 社交网络分析:识别用户群体(如微信群组)。
  • 电路设计:检测电路板的连通区域。
  • 图像处理:分割图像中的连通像素区域。

6. 常见问题解答

Q1: 如何判断图是否连通? 检查components的长度是否为1:

python 复制代码
def is_connected(graph):
    return len(connected_components(graph)) == 1

Q2: 如何处理有向图的连通性? 有向图的连通性分为:

  • 弱连通:忽略方向后无向图连通(可用本算法)。
  • 强连通:需用Kosaraju/Tarjan算法。

Q3: 如何优化大规模图的性能?

  • 使用迭代DFS/BFS避免递归栈溢出。
  • 并行化处理不同连通分量。

2. 强连通分量(Kosaraju算法)

python 复制代码
def kosaraju(graph):
    # 第一次DFS:记录顶点处理完成顺序
    visited = set()
    order = []
    
    def dfs(u):
        stack = [(u, False)]  # (顶点, 是否已处理)
        while stack:
            node, processed = stack.pop()
            if processed:
                order.append(node)  # 后序记录
                continue
            if node in visited:
                continue
            visited.add(node)
            stack.append((node, True))  # 标记为已处理
            for v, _ in graph.get(node, []):  # 遍历邻居
                if v not in visited:
                    stack.append((v, False))
    
    # 对每个未访问顶点执行DFS
    for u in graph:
        if u not in visited:
            dfs(u)
    
    # 构建反向图
    reversed_graph = {}
    for u in graph:
        for v, w in graph[u]:
            reversed_graph.setdefault(v, []).append((u, w))
    
    # 第二次DFS:按逆序处理反向图
    visited = set()
    scc = []  # 存储强连通分量
    
    for u in reversed(order):  # 按第一次DFS的逆序处理
        if u not in visited:
            stack = [u]
            visited.add(u)
            component = []
            while stack:
                node = stack.pop()
                component.append(node)
                for v, _ in reversed_graph.get(node, []):
                    if v not in visited:
                        visited.add(v)
                        stack.append(v)
            scc.append(component)
    return scc

1. 核心步骤图示

假设有以下有向图

python 复制代码
graph = {
    'A': [('B', 1)],
    'B': [('C', 1), ('E', 1)],
    'C': [('D', 1)],
    'D': [('A', 1), ('C', 1)],
    'E': [('F', 1)],
    'F': [('G', 1)],
    'G': [('E', 1)],
    'H': [('I', 1)],
    'I': []
}
图示
python 复制代码
A → B → C ←→ D
      ↘
        E ←→ F ←→ G
H → I

2. 执行过程分步演示

步骤1:第一次DFS(记录处理顺序)
  • DFS顺序 (后序遍历):
    1. 从A出发:访问A→B→C→D(D完成后回溯)
      • 记录顺序:D, C, B, A
    2. 从E出发:访问E→F→G(G完成后回溯)
      • 记录顺序:G, F, E
    3. 从H出发:访问H→I
      • 记录顺序:I, H
  • 最终order['D', 'C', 'B', 'A', 'G', 'F', 'E', 'I', 'H']
步骤2:构建反向图
python 复制代码
A ← B ← C ←→ D
      ↖
        E ←→ F ←→ G
H ← I
步骤3:第二次DFS(按order逆序处理反向图)
当前顶点 连通分量 动作说明
H [H] H无反向边,独立SCC
I [I] I无反向边,独立SCC
E [E, G, F] E→G→F→E形成SCC
A [A, D, C, B] A←B←C←D←A形成SCC

最终SCC结果

python 复制代码
[
    ['A', 'D', 'C', 'B'],  # A-B-C-D强连通
    ['E', 'G', 'F'],       # E-F-G强连通
    ['H'],                 # 独立顶点
    ['I']                  # 独立顶点
]

3. 关键点说明

  1. 两次DFS的必要性
    • 第一次DFS确定顶点的处理顺序(保证反向图中SCC的拓扑序)。
    • 第二次DFS在反向图中识别SCC。
  2. 反向图的作用:将原图的边反向,使得SCC内部的顶点仍能互相访问,而不同SCC之间隔离。
  3. 时间复杂度:O(V + E),每个顶点和边被处理两次。

4. 与Tarjan算法的对比

特性 Kosaraju Tarjan
DFS次数 2次 1次
空间复杂度 O(V + E)(需存储反向图) O(V)(仅维护栈和lowlink)
适用场景 代码更直观 更高效

5. 应用场景

  • 编译器优化:识别代码中的循环依赖。
  • 社交网络:发现紧密互动的用户群体。
  • 网页链接分析:确定网页社区的强关联子集。

6. 常见问题解答

Q1: 为什么第一次DFS要用后序遍历? 后序遍历确保在反向图中,一个SCC的顶点会在其依赖的其他SCC之前被处理。

Q2: 如何处理非强连通的有向图? 算法会自动将图分解为多个SCC,非强连通的部分会作为独立SCC返回。

Q3: 如何验证结果的正确性? 检查:

  1. 每个SCC内部任意两顶点双向可达。
  2. 不同SCC之间无双向路径。

3. 最大流问题(Ford-Fulkerson算法)

python 复制代码
from collections import deque

def ford_fulkerson(graph, source, sink):
    # 1. 初始化残量图(包含反向边)
    residual = {u: {} for u in graph}
    for u in graph:
        for v, cap in graph[u]:
            residual[u][v] = cap  # 正向边初始容量
            residual[v][u] = 0    # 反向边初始为0

    parent = {}      # 记录增广路径
    max_flow = 0     # 最大流结果

    # 2. BFS寻找增广路径
    def bfs():
        visited = set()
        queue = deque([source])
        visited.add(source)
        while queue:
            u = queue.popleft()
            for v in residual[u]:
                if v not in visited and residual[u][v] > 0:
                    visited.add(v)
                    parent[v] = u
                    if v == sink:
                        return True  # 找到增广路径
                    queue.append(v)
        return False  # 无增广路径

    # 3. 不断寻找增广路径并更新残量图
    while bfs():
        # 计算路径最小残量
        path_flow = float('inf')
        v = sink
        while v != source:
            u = parent[v]
            path_flow = min(path_flow, residual[u][v])
            v = u

        # 更新残量图(正向边减,反向边加)
        v = sink
        while v != source:
            u = parent[v]
            residual[u][v] -= path_flow
            residual[v][u] += path_flow
            v = u

        max_flow += path_flow

    return max_flow

1. 核心步骤图示

假设有以下流网络(邻接表表示):

python 复制代码
graph = {
    's': [('a', 3), ('b', 2)],  # s为源点
    'a': [('c', 3), ('d', 2)],
    'b': [('d', 3)],
    'c': [('t', 2)],            # t为汇点
    'd': [('t', 3)],
    't': []
}

初始残量图

python 复制代码
       3
     a → c
   ↗     ↘
s         t
   ↘     ↗
     b → d
       3

2. 执行过程分步演示

第一次迭代:
  • BFS路径 :s → a → c → t
    • 路径最小残量:min(3, 3, 2) = 2
    • 更新残量:
      • s-a: 3→1, a-c: 3→1, c-t: 2→0
      • 反向边:a-s: 0→2, c-a: 0→2, t-c: 0→2
  • 当前流量:0 → 2
第二次迭代:
  • BFS路径 :s → a → d → t
    • 路径最小残量:min(1, 2, 3) = 1
    • 更新残量:
      • s-a: 1→0, a-d: 2→1, d-t: 3→2
      • 反向边:a-s: 2→3, d-a: 0→1, t-d: 0→1
  • 当前流量:2 → 3
第三次迭代:
  • BFS路径 :s → b → d → t
    • 路径最小残量:min(2, 3, 2) = 2
    • 更新残量:
      • s-b: 2→0, b-d: 3→1, d-t: 2→0
      • 反向边:b-s: 0→2, d-b: 0→2, t-d: 1→3
  • 当前流量:3 → 5
终止条件
  • 无法再找到从 s 到 t 的增广路径
  • 最终最大流:5

3. 关键点说明

  1. 残量图(Residual Graph)
    • 正向边:剩余容量 = 原始容量 - 已用流量
    • 反向边:允许"撤销"流量(实现贪心算法的回退)
  2. 增广路径:残量图中从源点到汇点的路径,其最小残量决定可增加的流量。
  3. 时间复杂度:O(E * max_flow),最坏情况下效率低(如边权为无理数时可能不终止)。

4. 与Edmonds-Karp算法的关系

Ford-Fulkerson 是框架,而 Edmonds-Karp 是其特例(规定用BFS找最短增广路径,时间复杂度优化为O(V E²))。

5. 应用场景

  • 交通网络:计算道路的最大通行能力。
  • 水管系统:确定输水管道的最大流量。
  • 数据流分析:网络数据传输的带宽优化。

6. 常见问题解答

Q1: 为什么需要反向边? 反向边允许算法"撤销"之前的流量分配,确保能找到全局最优解(如允许流量从a→b后又从b→a回流)。

Q2: 如何记录具体的流量分配方案? 在更新残量图时,记录原始边的流量变化:

python 复制代码
flow = {u: {v: 0 for v in graph} for u in graph}
# 在更新残量图时同步:
flow[u][v] += path_flow

Q3: 如何处理多源点/多汇点问题? 添加超级源点和超级汇点:

  • 超级源点连接到所有源点,容量为无穷大。
  • 所有汇点连接到超级汇点,容量为无穷大。

图算法复杂度对比

算法 时间复杂度 空间复杂度 适用场景
BFS/DFS O(V+E) O(V) 连通性检测、最短路径
Dijkstra O((V+E)log V) O(V) 非负权单源最短路径
Bellman-Ford O(VE) O(V) 含负权单源最短路径
Floyd-Warshall O(V³) O(V²) 所有顶点对最短路径
Prim O(E log V) O(V) 无向图最小生成树
Kruskal O(E log E) O(E) 无向图最小生成树
Topological Sort O(V+E) O(V) 任务调度、编译顺序
Kosaraju SCC O(V+E) O(V) 强连通分量检测
Ford-Fulkerson O(E * max_flow) O(V²) 网络流问题

图数据库与处理框架

1. 图数据库

  • Neo4j:最流行的原生图数据库
  • JanusGraph:可扩展的分布式图数据库
  • Amazon Neptune:全托管的图数据库服务

2. 图处理框架

  • Apache Giraph:基于Hadoop的图处理
  • GraphX:Spark的图计算API
  • NetworkX:Python图分析库

常见面试问题

  1. 克隆图(深拷贝)
  2. 课程表安排(拓扑排序)
  3. 岛屿数量(连通分量)
  4. 单词接龙(最短路径)
  5. 朋友圈(并查集)
  6. 网络延迟时间(Dijkstra)
  7. 最小高度树(图中心)
  8. 重新安排行程(欧拉路径)
  9. 除法求值(加权图)
  10. 水管网络(最大流)

学习建议

  1. 可视化工具:使用Graphviz或在线工具观察图结构
  2. 分步调试:手动模拟算法执行过程
  3. 比较学习:对比不同算法的适用场景
  4. 实际问题:将社交网络等现实问题抽象为图
  5. 竞赛练习:LeetCode/Codeforces图论题目

总结:图的思维模式

图论不仅是一组算法,更是一种强大的建模工具:

  • 抽象能力:将复杂系统简化为节点和边
  • 算法思维:掌握经典范型(贪心、动态规划等)
  • 跨领域应用:从社交网络到生物信息学

掌握图的关键在于:

  1. 理解不同表示方法的优缺点
  2. 熟练核心算法及其变体
  3. 培养将实际问题转化为图问题的能力
  4. 了解现代图处理系统和框架

记住:图是描述关系的通用语言,掌握这种语言将为你打开解决复杂问题的新视角。无论是系统设计还是算法优化,图结构都将继续在计算机科学的未来扮演核心角色。

相关推荐
初学小刘19 分钟前
机器学习中的聚类与集成算法:从基础到应用
算法·机器学习·聚类
杜子不疼.1 小时前
【LeetCode 415】—字符串相加算法详解
算法·leetcode·职场和发展
仙俊红1 小时前
LeetCode每日一题,2025-08-21
算法·leetcode·职场和发展
楽码1 小时前
傻傻分不清:信息和通信复杂度
网络协议·算法·函数式编程
凳子(刘博浩)1 小时前
机器学习两大核心算法:集成学习与 K-Means 聚类详解
算法·机器学习·集成学习
已读不回1432 小时前
设计模式-工厂模式
前端·算法·代码规范
CoovallyAIHub2 小时前
YOLOv8-SMOT:基于切片辅助训练与自适应运动关联的无人机视角小目标实时追踪框架
深度学习·算法·计算机视觉
CoovallyAIHub2 小时前
全景式综述|多模态目标跟踪全面解析:方法、数据、挑战与未来
深度学习·算法·计算机视觉
像风一样自由20202 小时前
五种算法详解(SVM / Logistic Regression / kNN / Random Forest / HistGradientBoosting)
算法·随机森林·支持向量机
Cyclic10012 小时前
IOS购买订阅通知信息解析说明Java
java·开发语言·ios