图论(Graph Theory)是离散数学中的重要领域,广泛应用于计算机科学、网络分析、社会网络等多个学科领域。图是一种数学结构,用来描述对象(顶点,Vertices)及其关系(边,Edges)。通过研究图的性质和相关算法,我们可以解决很多实际问题。
本文将从图的基本概念、表示方法、特殊图和常用算法几个方面介绍图论的基础知识。
给定一个无向图,求图中边的总数和顶点度数的关系。
根据无向图的性质,图中的每一条边都会连接两个顶点,因此所有顶点的度数之和等于边数的两倍。设图中有 条边和 个顶点,顶点 的度数为 ,则有:
这称为图论中的手摇定理。
邻接矩阵(Adjacency Matrix):使用一个 的矩阵来表示图,其中 表示顶点 和顶点 之间有边,否则为 0。
例子:对于下图中的无向图:
(1)---(2)---(3) | | (4)---(5)---(6)
其邻接矩阵为:
邻接表(Adjacency List):对于每个顶点,记录它所连接的顶点集合。邻接表更加节省空间,适用于稀疏图。
例子:同样对于上图,其邻接表为:
1: 2, 4 2: 1, 3 3: 2, 6 4: 1, 5 5: 4, 6 6: 3, 5
边集(Edge List):记录图中所有的边。每一条边表示为两个顶点之间的连接。
例子:对于上图,边集为:
(1, 2), (1, 4), (2, 3), (3, 6), (4, 5), (5, 6)
深度优先搜索(DFS, Depth First Search): 深度优先搜索是一种遍历或搜索图的算法,它从起点开始尽可能深入每一条路径,直到无法继续为止,然后回溯继续搜索未访问的路径。DFS 通常用递归实现。
伪代码:
DFS(v): 标记 v 已访问 对 v 的每一个邻居 w: 如果 w 未访问: DFS(w)
例题:给定一个图,判断图中是否存在环。
解法:可以使用 DFS,若在遍历过程中遇到一个已经访问的顶点且该顶点不是当前节点的父节点,则图中存在环。
广度优先搜索(BFS, Breadth First Search): 广度优先搜索从起点开始,按层次逐层访问所有顶点,通常使用队列实现。
伪代码:
BFS(v): 初始化队列 Q 将 v 入队并标记为已访问 当 Q 不为空时: 取出队首节点 u 对 u 的每一个邻居 w: 如果 w 未访问: 将 w 入队并标记为已访问
Dijkstra算法:用于求解单源最短路径问题,即从某个顶点出发,到其他所有顶点的最短路径。该算法要求图中不存在负权边。
例题:给定一个加权无向图,求从某一顶点到其他所有顶点的最短路径。
解法:Dijkstra 算法每次选取当前未处理的距离最短的顶点,更新其邻居的最短路径,直到所有顶点都处理完毕。
Kruskal算法:用于求解最小生成树问题。该算法每次选择权值最小的边,且不会形成环,直到所有顶点连通。
伪代码:
Kruskal(G): 初始化生成树 T = 空集 对图 G 中的所有边按权重从小到大排序 对每条边 (u, v): 如果 u 和 v 不在同一个连通分量中: 将边 (u, v) 加入生成树 T 合并 u 和 v 所在的连通分量
Prim算法:另一种用于求解最小生成树的算法,Prim算法从某个顶点出发,每次选取一条最小权值的边将一个新顶点加入生成树,直到所有顶点都被包括。
深度优先搜索(Depth First Search,简称DFS)是一种用于遍历或搜索图(Graph)或树(Tree)结构的算法。它的核心思想是尽可能深地搜索图的分支,直到不能再深入为止,然后回溯,继续搜索未访问的节点。DFS常用于解决许多图算法问题,如连通性检测、路径搜索和拓扑排序等。
DFS可以使用递归或栈来实现。递归方式更加简洁,而使用栈的非递归方式可以避免因递归过深导致的栈溢出。
pythondef dfs(graph, start, visited=None):
if visited is None:
visited = set() # 创建一个集合来存储访问过的节点
visited.add(start)
print(start) # 打印或处理当前节点
for next_node in graph[start] - visited:
dfs(graph, next_node, visited)
return visited
在这个实现中,graph
是图的邻接表表示,start
是起始节点,visited
是已访问的节点集合。
pythondef dfs_iterative(graph, start):
visited = set() # 存储已访问的节点
stack = [start] # 使用栈来模拟递归
while stack:
node = stack.pop()
if node not in visited:
visited.add(node)
print(node) # 打印或处理当前节点
# 将所有未访问的相邻节点加入栈中
stack.extend(graph[node] - visited)
return visited
DFS是一种基本但非常重要的图算法,适合在需要深入探索、回溯的场景下使用。
广度优先搜索(Breadth First Search,简称BFS)是一种用于遍历或搜索图(Graph)或树(Tree)结构的算法。其基本思想是从起始节点开始,先访问所有邻接节点,然后再依次访问这些邻接节点的邻接节点,层层推进,直到访问完所有可达节点为止。BFS通常用于寻找最短路径、图的连通性和层次遍历等问题。
BFS通常使用队列来实现,以保证先访问的节点会先被处理。
pythonfrom collections import deque
def bfs(graph, start):
visited = set() # 存储已访问的节点
queue = deque([start]) # 初始化队列
visited.add(start)
while queue:
node = queue.popleft() # 从队列中取出节点
print(node) # 打印或处理当前节点
for neighbor in graph[node]:
if neighbor not in visited:
visited.add(neighbor) # 标记为已访问
queue.append(neighbor) # 将未访问的邻接节点加入队列
return visited
在这个实现中,graph
是图的邻接表表示,start
是起始节点,visited
是已访问的节点集合。
BFS是一种重要且实用的图搜索算法,适合在需要层次遍历或最短路径查找的场景下使用。
Dijkstra算法是一种用于解决最短路径问题的经典算法,尤其适用于权重为非负数的图(Graph)。该算法以荷兰计算机科学家艾兹赫尔·迪克斯特拉(Edsger Dijkstra)的名字命名,能够有效地找到从一个起始节点到其他所有节点的最短路径。
Dijkstra算法的基本思想是通过逐步扩展已知的最短路径,利用贪心策略,在每一步中选择当前最短的路径进行扩展。算法维护一个最短路径的估计值,对于每个节点,在访问其邻接节点时更新这些估计值。
初始化:
处理节点:
重复处理:重复步骤2,直到优先队列为空。
输出结果:最终,距离数组中的值即为从起始节点到其他所有节点的最短距离。
以下是Dijkstra算法的Python实现示例:
pythonimport heapq
def dijkstra(graph, start):
# 初始化距离字典
distances = {node: float('infinity') for node in graph}
distances[start] = 0
priority_queue = [(0, start)] # (距离, 节点)
while priority_queue:
current_distance, current_node = heapq.heappop(priority_queue)
# 如果当前距离大于已知最短距离,则跳过
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(priority_queue, (distance, neighbor))
return distances
在这个实现中,graph
是一个字典,表示图的邻接表,start
是起始节点。
Dijkstra算法是解决最短路径问题的重要工具,其贪心策略和有效的实现方式使其广泛应用于多个领域。
Kruskal算法是一种用于求解最小生成树(Minimum Spanning Tree, MST)的贪心算法,适用于加权无向图。该算法通过逐步选择边,确保每一步选择都是当前可用边中权重最小的,且不会形成环,从而构造出一棵覆盖所有节点的最小生成树。
Kruskal算法的核心思想是通过选择边来连接图中的节点,始终选择权重最小的边,直到所有节点都被连接为止。使用并查集(Union-Find)数据结构来管理已连接的组件,以确保不形成环。
初始化:
边的选择:
重复选择:重复步骤2,直到生成树中的边数达到(为图中的节点数)。
以下是Kruskal算法的Python实现示例:
pythonclass DisjointSet:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * n
def find(self, u):
if self.parent[u] != u:
self.parent[u] = self.find(self.parent[u]) # 路径压缩
return self.parent[u]
def union(self, u, v):
root_u = self.find(u)
root_v = self.find(v)
if root_u != root_v:
if self.rank[root_u] > self.rank[root_v]:
self.parent[root_v] = root_u
elif self.rank[root_u] < self.rank[root_v]:
self.parent[root_u] = root_v
else:
self.parent[root_v] = root_u
self.rank[root_u] += 1
def kruskal(vertices, edges):
# edges是一个列表,包含边的信息 (权重, 起始节点, 终止节点)
edges.sort() # 按权重排序
mst = []
disjoint_set = DisjointSet(len(vertices))
for weight, u, v in edges:
if disjoint_set.find(u) != disjoint_set.find(v):
disjoint_set.union(u, v)
mst.append((u, v, weight)) # 添加边到生成树
return mst
在这个实现中,vertices
是节点列表,edges
是边的列表,包含每条边的权重和连接的节点。
Kruskal算法是一种经典的生成树算法,通过有效地选择边,确保了生成树的最小权重和连通性,广泛应用于网络设计和图形处理等领域。
Prim算法是一种用于求解最小生成树(Minimum Spanning Tree, MST)的贪心算法,适用于加权无向图。与Kruskal算法不同,Prim算法从一个起始节点开始逐步扩展生成树,始终选择当前可用边中权重最小的边,从而连接未被访问的节点。
Prim算法的核心思想是逐步构建最小生成树,始终从当前已构建的树中选择最小的边,连接新节点,直到所有节点都被连接为止。该算法适合于密集图,因为它在扩展树的过程中可以保持对所有边的访问。
初始化:
边的选择:
重复选择:重复步骤2,直到生成树包含所有节点。
以下是Prim算法的Python实现示例:
pythonimport heapq
def prim(graph, start):
mst = [] # 最小生成树
visited = set() # 记录已访问的节点
min_heap = [(0, start)] # (边的权重, 起始节点)
while min_heap:
weight, u = heapq.heappop(min_heap)
if u in visited:
continue # 如果节点已访问,跳过
visited.add(u) # 标记节点为已访问
mst.append((weight, u)) # 添加边到生成树
for v, edge_weight in graph[u]: # 遍历当前节点的所有邻接边
if v not in visited:
heapq.heappush(min_heap, (edge_weight, v)) # 将未访问的边加入优先队列
return mst
在这个实现中,graph
是一个字典,表示图的邻接表,每个节点的值是一个列表,包含相邻节点及其边权重,start
是起始节点。
Prim算法是一种高效的生成树算法,通过选择权重最小的边来逐步构建最小生成树,广泛应用于网络设计、图形处理和交通规划等领域。
并查集(Union-Find)是一种用于处理不相交集合(Disjoint Set)的数据结构,广泛应用于图论、网络连通性、群体划分等问题。它支持两种基本操作:合并(Union)和查找(Find),并且通过优化可以达到接近常数时间的复杂度。
查找(Find):
合并(Union):
并查集通常使用一个数组来存储每个元素的父节点,以及一个额外的数组来记录每个树的秩(或大小):
parent[i]
表示元素i
的父节点。rank[i]
表示以元素i
为根的树的高度。以下是并查集的Python实现示例:
pythonclass DisjointSet:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * n
def find(self, u):
if self.parent[u] != u:
self.parent[u] = self.find(self.parent[u]) # 路径压缩
return self.parent[u]
def union(self, u, v):
root_u = self.find(u)
root_v = self.find(v)
if root_u != root_v:
# 按秩合并
if self.rank[root_u] > self.rank[root_v]:
self.parent[root_v] = root_u
elif self.rank[root_u] < self.rank[root_v]:
self.parent[root_u] = root_v
else:
self.parent[root_v] = root_u
self.rank[root_u] += 1
并查集是一种高效的集合管理数据结构,通过优化策略实现快速的查找和合并操作,广泛应用于计算机科学的多个领域。
图论是离散数学的重要组成部分,拥有广泛的应用场景。从基本的图结构、表示方法到复杂的图算法,图论为解决现实问题提供了强有力的工具。在深入学习这些基础知识的同时,理解它们的实际应用将极大提升解决问题的能力。
通过上述介绍,读者可以更好地理解图论的基本概念,并通过例题巩固对图结构和算法的理解。如果你有任何问题或想法,欢迎在评论区讨论!
本文作者:Dong
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC。本作品采用《知识共享署名-非商业性使用 4.0 国际许可协议》进行许可。您可以在非商业用途下自由转载和修改,但必须注明出处并提供原作者链接。 许可协议。转载请注明出处!