第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 小结
本章通过图示和代码补充了图的最小生成树、最短路径、拓扑排序等核心算法的详细实现,并结合实际案例展示了它们的应用场景。通过对这些算法的学习,可以清晰地理解图在交通规划、任务调度、网络分析等领域的重要性。