第6章 图
6.1 图的定义和基本术语
6.1.1 图的定义
图(Graph)是由顶点的有限非空集合和顶点之间边的集合组成的数据结构。一个图通常表示为 G = ( V , E ) G=(V, E) G=(V,E),其中:
- V V V 是顶点(Vertex)的集合。
 - E E E 是边(Edge)的集合。
 
根据边是否有方向,图可以分为:
- 无向图 :边没有方向,通常表示为 e = { u , v } e = \{u, v\} e={u,v}。
 - 有向图 :边有方向,通常表示为 e = ( u , v ) e = (u, v) e=(u,v)。
 
此外,图还可以是带权图,即每条边或弧上附有一个权值。
6.1.2 图的基本术语
- 
邻接点:如果两个顶点之间有一条边相连,则这两个顶点互为邻接点。
 - 
度:
- 在无向图中,顶点的度是与该顶点相关联的边的数目。
 - 在有向图中,顶点的入度是指指向该顶点的边的数目,出度是指从该顶点出发的边的数目。
 
 - 
路径:路径是指从一个顶点到另一个顶点的一个顶点序列。
- 简单路径:路径中不重复经过顶点。
 - 回路或环:起点和终点相同的路径。
 
 - 
连通性:
- 在无向图中,如果任意两个顶点之间存在路径,则称该图为连通图。
 - 在有向图中,如果任意两个顶点之间存在有向路径,则称为强连通图。
 
 - 
完全图:在完全图中,任意两个顶点之间都存在边。
 - 
子图 :由图 G G G 的部分顶点和边组成的图。
 
6.2 案例引入
问题描述
考虑城市交通网络的建模。一个城市的地铁站可以看作图的顶点,两个地铁站之间的地铁线路可以看作边。建模的目标是:
- 计算最短路径(如从 A 站到 B 站的最短车程)。
 - 寻找最小生成树(构建连接所有站点的最低成本方案)。
 - 进行图的遍历(确定从任意一个站点出发是否可以覆盖所有站点)。
 
6.3 图的类型定义
图可以根据边的方向和权值分类为以下类型:
- 无向图:边没有方向。
 - 有向图 :边有方向,表示为 ( u , v ) (u, v) (u,v)。
 - 带权图:每条边有一个权值,用于表示距离、成本等信息。
 - 稀疏图:边的数量远小于顶点的数量。
 - 稠密图:边的数量接近于顶点数量的平方。
 
6.4 图的存储结构
6.4.1 邻接矩阵
邻接矩阵是一种二维数组,用于存储图中顶点之间的连接关系。
定义
- 无向图 :若 G G G 中有边 { i , j } \{i, j\} {i,j},则 A [ i ] [ j ] = 1 A[i][j] = 1 A[i][j]=1;否则 A [ i ] [ j ] = 0 A[i][j] = 0 A[i][j]=0。
 - 有向图 :若 G G G 中有边 ( i , j ) (i, j) (i,j),则 A [ i ] [ j ] = 1 A[i][j] = 1 A[i][j]=1;否则 A [ i ] [ j ] = 0 A[i][j] = 0 A[i][j]=0。
 
图示
无向图:
            
            
              css
              
              
            
          
              A---B
     \  |
      C-D
        邻接矩阵表示:
| A | B | C | D | |
|---|---|---|---|---|
| A | 0 | 1 | 1 | 0 | 
| B | 1 | 0 | 0 | 1 | 
| C | 1 | 0 | 0 | 1 | 
| D | 0 | 1 | 1 | 0 | 
优缺点
- 优点:快速判断顶点间是否有边。
 - 缺点:浪费存储空间(稀疏图中会有很多无用的 0)。
 
6.4.2 邻接表
邻接表是一种链表形式的存储方式,用于存储每个顶点的邻接顶点。
图示
无向图:
            
            
              css
              
              
            
          
              A---B
     \  |
      C-D
        邻接表表示:
            
            
              rust
              
              
            
          
          A -> B -> C
B -> A -> D
C -> A -> D
D -> B -> C
        优缺点
- 优点:适合稀疏图,节省存储空间。
 - 缺点:判断顶点间是否有边比较慢。
 
6.4.3 十字链表
十字链表是一种存储有向图的结构,结合了邻接表和逆邻接表的信息。
6.4.4 邻接多重表
邻接多重表是一种适合存储无向图的结构,用一个边结点同时存储两个顶点的信息。
6.5 图的遍历
6.5.1 深度优先搜索(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)
        6.5.2 广度优先搜索(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)
        6.6 图的应用
6.6.1 最小生成树(MST)
最小生成树(Minimum Spanning Tree)是图的一个子图,它包含所有的顶点,并且总边权值最小,且没有环。
常用算法
- 
Prim 算法:从某个顶点开始,逐步扩展最小权重的边,直到包含所有顶点。
- 
图示
无向加权图:
cssA / | \ 1 3 4 / | \ B----C----D 2步骤:
- 从顶点 A 开始,选择边权最小的 AB。
 - 扩展到未访问的顶点 C,选择权重为 2 的边 BC。
 - 最后添加 CD(权重为 3)。最小生成树:
 
cssA / 1 / B----C 2 \ 3 \ D 
 - 
 - 
Kruskal 算法 :将所有边按权值从小到大排序,逐一选择不会形成环的边,直到构建最小生成树。应用: 网络规划、光纤布局等。
 
6.6.2 最短路径
Dijkstra 算法
用于求解单源最短路径问题,要求所有边权为非负。
图示:
- 无向加权图:
 
            
            
              css
              
              
            
          
              A --2-- B
    |       |
   1|       |3
    |       |
    C --1-- D
        - 
步骤:
- 初始化:设起点为 A,初始距离为 ∞ \infty ∞。
 - 选择最短距离的顶点依次更新其他顶点距离。
 
 
最终结果:
| 顶点 | A -> 距离 | 路径 | 
|---|---|---|
| A | 0 | A | 
| B | 2 | A->B | 
| C | 1 | A->C | 
| D | 2 | A->C->D | 
代码实现:
            
            
              python
              
              
            
          
          import heapq
def dijkstra(graph, start):
    pq = []  # 优先队列
    heapq.heappush(pq, (0, start))
    distances = {node: float('inf') for node in graph}
    distances[start] = 0
    while pq:
        current_distance, current_node = heapq.heappop(pq)
        if current_distance > distances[current_node]:
            continue
        for neighbor, weight in graph[current_node].items():
            distance = current_distance + weight
            if distance < distances[neighbor]:
                distances[neighbor] = distance
                heapq.heappush(pq, (distance, neighbor))
    return distances
        应用场景
- 地图导航系统(如 Google Maps)。
 - 网络路由协议(如 OSPF 协议)。
 
6.6.3 拓扑排序
拓扑排序用于对有向无环图(DAG)进行线性排序,使得每条有向边 ( u , v ) (u, v) (u,v) 中顶点 u u u 排在 v v v 前面。
算法步骤
- 计算每个顶点的入度。
 - 将入度为 0 的顶点加入队列。
 - 从队列中取出顶点,输出到拓扑序列,更新邻接点的入度。
 - 重复上述步骤,直到队列为空。
 
图示
            
            
              mathematica
              
              
            
          
          A → B → C
↓
D → E
        - 初始入度:顶点入度A0B1C1D1E2
 - 拓扑排序:A → B → D → C → E
 
应用场景
- 课程安排(课程依赖关系)。
 - 项目管理中的任务调度。
 
6.6.4 关键路径
关键路径(Critical Path)是项目管理中的重要工具,用于分析项目中最重要的任务序列。
图示
任务节点:
            
            
              scss
              
              
            
          
          (开始) → A(3天) → B(2天) → C(4天) → (结束)
           ↓
           D(6天)
        分析步骤
- 计算每个节点的最早完成时间(Early Time, ET)。
 - 计算每个节点的最迟完成时间(Late Time, LT)。
 - 找到 ET 和 LT 相等的路径,即为关键路径。
关键路径:A → D 
6.7 案例分析与实现
案例1:最小生成树的实现(Prim 算法)
输入图:
            
            
              mathematica
              
              
            
          
              1        3
A ------- B ------ C
|         |       |
2         4       5
|         |       |
D ------- E ------ F
      6
        代码实现:
            
            
              python
              
              
            
          
          import heapq
def prim(graph, start):
    mst = []  # 最小生成树的边
    visited = set()
    pq = [(0, start, None)]  # (权重, 当前顶点, 前驱顶点)
    
    while pq:
        weight, current, prev = heapq.heappop(pq)
        if current in visited:
            continue
        visited.add(current)
        if prev is not None:
            mst.append((prev, current, weight))
        
        for neighbor, edge_weight in graph[current].items():
            if neighbor not in visited:
                heapq.heappush(pq, (edge_weight, neighbor, current))
    
    return mst
        输入数据:
            
            
              python
              
              
            
          
          graph = {
    'A': {'B': 1, 'D': 2},
    'B': {'A': 1, 'C': 3, 'E': 4},
    'C': {'B': 3, 'F': 5},
    'D': {'A': 2, 'E': 6},
    'E': {'B': 4, 'D': 6, 'F': 5},
    'F': {'C': 5, 'E': 5}
}
print(prim(graph, 'A'))
        输出结果:
            
            
              css
              
              
            
          
          [('A', 'B', 1), ('A', 'D', 2), ('B', 'E', 4), ('E', 'F', 5), ('B', 'C', 3)]
        案例2:最短路径(Dijkstra 算法)
输入图:
            
            
              css
              
              
            
          
              A --2-- B
    |      /|
   1|   3  |
    |  /    |
    C --1-- D
        代码实现:
            
            
              python
              
              
            
          
          graph = {
    'A': {'B': 2, 'C': 1},
    'B': {'A': 2, 'C': 3, 'D': 3},
    'C': {'A': 1, 'B': 3, 'D': 1},
    'D': {'B': 3, 'C': 1}
}
print(dijkstra(graph, 'A'))
        输出结果:
            
            
              css
              
              
            
          
          {'A': 0, 'B': 2, 'C': 1, 'D': 2}
        6.8 小结
本章通过图示和代码补充了图的最小生成树、最短路径、拓扑排序等核心算法的详细实现,并结合实际案例展示了它们的应用场景。通过对这些算法的学习,可以清晰地理解图在交通规划、任务调度、网络分析等领域的重要性。