2024-09-24
数学之美
00

目录

图论概述
图的基本概念
例题:顶点度数与边数的关系
图的表示
图算法
深度优先搜索
DFS的基本步骤:
实现方式:
递归实现:
非递归实现(使用栈):
深度优先搜索的特性:
DFS的应用:
与广度优先搜索(BFS)的对比:
广度优先搜索
BFS的基本步骤:
实现方式:
队列实现:
广度优先搜索的特性:
BFS的应用:
与深度优先搜索(DFS)的对比:
Dijkstra算法
算法基本思想
Dijkstra算法的步骤:
算法实现:
算法复杂度:
Dijkstra算法的应用:
注意事项:
Kruskal算法
算法基本思想
Kruskal算法的步骤:
算法实现:
算法复杂度:
Kruskal算法的应用:
注意事项:
Prim算法
算法基本思想
Prim算法的步骤:
算法实现:
算法复杂度:
Prim算法的应用:
注意事项:
并查集
基本操作
数据结构
并查集的实现示例
算法复杂度
应用场景
优点与局限性
结语

图论概述

图论(Graph Theory)是离散数学中的重要领域,广泛应用于计算机科学、网络分析、社会网络等多个学科领域。图是一种数学结构,用来描述对象(顶点,Vertices)及其关系(边,Edges)。通过研究图的性质和相关算法,我们可以解决很多实际问题。

本文将从图的基本概念、表示方法、特殊图和常用算法几个方面介绍图论的基础知识。

图的基本概念

  • 顶点(Vertex):图中的基本元素,通常表示一个对象。例如,网络中的每个节点、城市中的每个地点都可以看作一个顶点。
  • 边(Edge):连接顶点之间的线段,表示顶点间的关系或路径。如果顶点之间有一条连接线,我们称这两个顶点是邻接的。
  • 度数(Degree):一个顶点的度数是与其相连的边的数目。对于无向图,顶点的度数即为连接它的边的数目;对于有向图,顶点有入度和出度之分。
  • 路径(Path):从一个顶点到另一个顶点经过的边序列。
  • 回路(Cycle):路径的起点和终点相同的路径称为回路。
  • 连通图(Connected Graph):如果图中的任意两个顶点之间都有路径相连,则该图为连通图。
  • 完全图(Complete Graph):图中任意两个顶点之间都有边相连,则该图为完全图。
  • 二分图(Bipartite Graph):顶点集可以分为两个不相交的子集,并且所有的边都连接两个不同子集的顶点,这样的图称为二分图。

例题:顶点度数与边数的关系

给定一个无向图,求图中边的总数和顶点度数的关系。

根据无向图的性质,图中的每一条边都会连接两个顶点,因此所有顶点的度数之和等于边数的两倍。设图中有 mm 条边和 nn 个顶点,顶点 viv_i 的度数为 d(vi)d(v_i),则有:

i=1nd(vi)=2m\sum_{i=1}^n d(v_i) = 2m

这称为图论中的手摇定理

图的表示

  • 邻接矩阵(Adjacency Matrix):使用一个 n×nn \times n 的矩阵来表示图,其中 A[i][j]=1A[i][j] = 1 表示顶点 ii 和顶点 jj 之间有边,否则为 0。

    例子:对于下图中的无向图:

    (1)---(2)---(3) | | (4)---(5)---(6)

    其邻接矩阵为:

    [010100101000010001100010000101001010]\begin{bmatrix} 0 & 1 & 0 & 1 & 0 & 0 \\ 1 & 0 & 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 & 0 & 1 \\ 1 & 0 & 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 & 0 & 1 \\ 0 & 0 & 1 & 0 & 1 & 0 \end{bmatrix}
  • 邻接表(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)

特殊图

  • 树(Tree):无环连通图,任意两个顶点之间有且仅有一条路径。
  • 二叉树(Binary Tree):每个顶点最多有两个子节点的树结构。
  • 生成树(Spanning Tree):给定图的一个子图,包含所有顶点且是一个树结构。
  • 最小生成树(Minimum Spanning Tree):在生成树中,所有边的权值和最小的生成树。

图算法

  1. 深度优先搜索(DFS, Depth First Search): 深度优先搜索是一种遍历或搜索图的算法,它从起点开始尽可能深入每一条路径,直到无法继续为止,然后回溯继续搜索未访问的路径。DFS 通常用递归实现。

    伪代码

    DFS(v): 标记 v 已访问 对 v 的每一个邻居 w: 如果 w 未访问: DFS(w)

    例题:给定一个图,判断图中是否存在环。

    解法:可以使用 DFS,若在遍历过程中遇到一个已经访问的顶点且该顶点不是当前节点的父节点,则图中存在环。

  2. 广度优先搜索(BFS, Breadth First Search): 广度优先搜索从起点开始,按层次逐层访问所有顶点,通常使用队列实现。

    伪代码

    BFS(v): 初始化队列 Q 将 v 入队并标记为已访问 当 Q 不为空时: 取出队首节点 u 对 u 的每一个邻居 w: 如果 w 未访问: 将 w 入队并标记为已访问
  3. Dijkstra算法:用于求解单源最短路径问题,即从某个顶点出发,到其他所有顶点的最短路径。该算法要求图中不存在负权边。

    例题:给定一个加权无向图,求从某一顶点到其他所有顶点的最短路径。

    解法:Dijkstra 算法每次选取当前未处理的距离最短的顶点,更新其邻居的最短路径,直到所有顶点都处理完毕。

  4. Kruskal算法:用于求解最小生成树问题。该算法每次选择权值最小的边,且不会形成环,直到所有顶点连通。

    伪代码

    Kruskal(G): 初始化生成树 T = 空集 对图 G 中的所有边按权重从小到大排序 对每条边 (u, v): 如果 u 和 v 不在同一个连通分量中: 将边 (u, v) 加入生成树 T 合并 u 和 v 所在的连通分量
  5. Prim算法:另一种用于求解最小生成树的算法,Prim算法从某个顶点出发,每次选取一条最小权值的边将一个新顶点加入生成树,直到所有顶点都被包括。

深度优先搜索

深度优先搜索(Depth First Search,简称DFS)是一种用于遍历或搜索图(Graph)或树(Tree)结构的算法。它的核心思想是尽可能深地搜索图的分支,直到不能再深入为止,然后回溯,继续搜索未访问的节点。DFS常用于解决许多图算法问题,如连通性检测、路径搜索和拓扑排序等。

DFS的基本步骤:

  1. 起始节点:从一个初始节点出发,标记为已访问。
  2. 深入探索:从当前节点开始,沿着一个未访问的相邻节点继续深入,重复这个过程,直到没有未访问的相邻节点。
  3. 回溯:如果当前节点没有未访问的相邻节点,返回到上一个节点,继续搜索未访问的相邻节点。
  4. 重复以上过程:直到所有节点都被访问过为止。

实现方式:

DFS可以使用递归来实现。递归方式更加简洁,而使用栈的非递归方式可以避免因递归过深导致的栈溢出。

递归实现:
python
def 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是已访问的节点集合。

非递归实现(使用栈):
python
def 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的时间复杂度为O(V+E)O(V+E),其中VV是图中的节点数,EE是图中的边数。
  • 空间复杂度:DFS的空间复杂度主要取决于递归深度或栈的大小,最坏情况下为O(V)O(V)

DFS的应用:

  1. 路径搜索:用于寻找从一个节点到另一个节点的路径,适用于有向图和无向图。
  2. 连通性检测:可以用于判断一个无向图是否为连通图,即是否所有节点都可以通过边连接。
  3. 拓扑排序:在有向无环图(DAG)中,可以利用DFS实现拓扑排序。
  4. 迷宫问题:DFS常用于解决迷宫问题,通过沿着路径深入探索来找到出口。

与广度优先搜索(BFS)的对比:

  • DFS优先探索深层次的节点,而BFS优先探索邻近的节点。
  • DFS适合用于需要深入探索、递归问题的场景,而BFS适合用于搜索最短路径或层次遍历的场景。

DFS是一种基本但非常重要的图算法,适合在需要深入探索、回溯的场景下使用。

广度优先搜索

广度优先搜索(Breadth First Search,简称BFS)是一种用于遍历或搜索图(Graph)或树(Tree)结构的算法。其基本思想是从起始节点开始,先访问所有邻接节点,然后再依次访问这些邻接节点的邻接节点,层层推进,直到访问完所有可达节点为止。BFS通常用于寻找最短路径、图的连通性和层次遍历等问题。

BFS的基本步骤:

  1. 起始节点:从一个初始节点出发,标记为已访问并加入队列。
  2. 访问邻接节点:从队列中取出一个节点,访问所有未被访问的相邻节点,并将它们标记为已访问后加入队列。
  3. 重复以上过程:继续从队列中取出节点并访问其相邻节点,直到队列为空。

实现方式:

BFS通常使用队列来实现,以保证先访问的节点会先被处理。

队列实现:
python
from 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的时间复杂度为O(V+E)O(V + E),其中VV是图中的节点数,EE是图中的边数。
  • 空间复杂度:BFS的空间复杂度主要取决于队列的大小,在最坏情况下为O(V)O(V),因为队列可能会存储所有节点。

BFS的应用:

  1. 最短路径搜索:在无权图中,BFS可以用于寻找两个节点之间的最短路径。
  2. 图的连通性检测:通过BFS,可以判断一个图是否为连通图,即是否所有节点都可以通过边连接。
  3. 层次遍历:BFS可以用于图的层次遍历,比如二叉树的层序遍历。
  4. 网络流问题:在某些网络流算法(如Ford-Fulkerson方法)中,BFS用于寻找增广路径。

与深度优先搜索(DFS)的对比:

  • BFS优先访问邻近的节点,而DFS优先深入探索。
  • BFS适合用于寻找最短路径或层次遍历,而DFS适合于需要深入探索的场景。

BFS是一种重要且实用的图搜索算法,适合在需要层次遍历或最短路径查找的场景下使用。

Dijkstra算法

Dijkstra算法是一种用于解决最短路径问题的经典算法,尤其适用于权重为非负数的图(Graph)。该算法以荷兰计算机科学家艾兹赫尔·迪克斯特拉(Edsger Dijkstra)的名字命名,能够有效地找到从一个起始节点到其他所有节点的最短路径。

算法基本思想

Dijkstra算法的基本思想是通过逐步扩展已知的最短路径,利用贪心策略,在每一步中选择当前最短的路径进行扩展。算法维护一个最短路径的估计值,对于每个节点,在访问其邻接节点时更新这些估计值。

Dijkstra算法的步骤:

  1. 初始化

    • 创建一个距离数组,存储每个节点到起始节点的当前最短距离。初始时,起始节点的距离为0,其他节点的距离为无穷大。
    • 创建一个优先队列,用于存储尚未访问的节点,按距离排序。
    • 将起始节点加入优先队列。
  2. 处理节点

    • 从优先队列中取出距离最小的节点,标记为已访问。
    • 更新其邻接节点的距离。如果通过当前节点到达邻接节点的距离小于已知的距离,则更新该邻接节点的距离,并将其加入优先队列。
  3. 重复处理:重复步骤2,直到优先队列为空。

  4. 输出结果:最终,距离数组中的值即为从起始节点到其他所有节点的最短距离。

算法实现:

以下是Dijkstra算法的Python实现示例:

python
import 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算法的时间复杂度为O((V+E)logV)O((V + E) \log V),其中VV是图中的节点数,EE是图中的边数。
  • 空间复杂度:主要由距离字典和优先队列占用,空间复杂度为O(V)O(V)

Dijkstra算法的应用:

  1. 地图导航:用于计算从一个地点到另一个地点的最短路径。
  2. 网络路由:在网络中确定数据包从源到目的地的最优传输路径。
  3. 图形分析:在图形中进行路径规划与优化问题。

注意事项:

  • Dijkstra算法不适用于含有负权边的图。如果图中存在负权边,通常需要使用Bellman-Ford算法。
  • 对于所有节点的最短路径问题,Dijkstra算法在起始节点的选择上非常灵活,但在某些特定情况下,使用Floyd-Warshall算法可能更为高效。

Dijkstra算法是解决最短路径问题的重要工具,其贪心策略和有效的实现方式使其广泛应用于多个领域。

Kruskal算法

Kruskal算法是一种用于求解最小生成树(Minimum Spanning Tree, MST)的贪心算法,适用于加权无向图。该算法通过逐步选择边,确保每一步选择都是当前可用边中权重最小的,且不会形成环,从而构造出一棵覆盖所有节点的最小生成树。

算法基本思想

Kruskal算法的核心思想是通过选择边来连接图中的节点,始终选择权重最小的边,直到所有节点都被连接为止。使用并查集(Union-Find)数据结构来管理已连接的组件,以确保不形成环。

Kruskal算法的步骤:

  1. 初始化

    • 将图中的所有边按权重进行排序。
    • 创建一个空的生成树集合(MST)。
    • 初始化并查集,以便在后续操作中管理连接的组件。
  2. 边的选择

    • 按权重从小到大遍历边列表。
    • 对于每条边,检查其连接的两个节点是否在同一集合中。
    • 如果不在同一集合中,选择该边并将其加入生成树,随后合并两个集合。
  3. 重复选择:重复步骤2,直到生成树中的边数达到V1V-1VV为图中的节点数)。

算法实现:

以下是Kruskal算法的Python实现示例:

python
class 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算法的时间复杂度为O(ElogE)O(E \log E),主要来源于边的排序和并查集的合并操作。由于EE的数量通常比V2V^2小,所以复杂度可以表示为O(ElogV)O(E \log V)
  • 空间复杂度:空间复杂度为O(V+E)O(V + E),存储节点和边的信息。

Kruskal算法的应用:

  1. 网络设计:用于设计通信网络、电力网络等,以确保成本最低且每个节点都被连接。
  2. 图的连通性分析:用于分析图的结构特性,特别是在大规模图中。
  3. 道路和基础设施规划:优化城市规划中的道路布局,确保成本效益。

注意事项:

  • Kruskal算法适用于边相对较少的稀疏图,对于稠密图,Prim算法可能更为高效。
  • 确保输入的边权重非负,以确保生成树的正确性。

Kruskal算法是一种经典的生成树算法,通过有效地选择边,确保了生成树的最小权重和连通性,广泛应用于网络设计和图形处理等领域。

Prim算法

Prim算法是一种用于求解最小生成树(Minimum Spanning Tree, MST)的贪心算法,适用于加权无向图。与Kruskal算法不同,Prim算法从一个起始节点开始逐步扩展生成树,始终选择当前可用边中权重最小的边,从而连接未被访问的节点。

算法基本思想

Prim算法的核心思想是逐步构建最小生成树,始终从当前已构建的树中选择最小的边,连接新节点,直到所有节点都被连接为止。该算法适合于密集图,因为它在扩展树的过程中可以保持对所有边的访问。

Prim算法的步骤:

  1. 初始化

    • 选择一个起始节点,并将其标记为已访问。
    • 创建一个空的生成树集合(MST)。
    • 初始化一个优先队列(通常用最小堆实现),用于存储当前已访问节点的所有边,并按权重排序。
  2. 边的选择

    • 从优先队列中取出权重最小的边,检查该边连接的节点是否已经在生成树中。
    • 如果未在生成树中,将该边加入生成树,并标记其连接的节点为已访问。
    • 将新连接节点的所有未访问边加入优先队列。
  3. 重复选择:重复步骤2,直到生成树包含所有节点。

算法实现:

以下是Prim算法的Python实现示例:

python
import 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算法的时间复杂度为O(ElogV)O(E \log V),其中EE是图中的边数,VV是图中的节点数。
  • 空间复杂度:空间复杂度为O(V+E)O(V + E),主要用于存储邻接表和优先队列。

Prim算法的应用:

  1. 网络设计:用于设计最小成本的通信网络、供电网络等,确保所有节点都能连接。
  2. 图形处理:在图形学中用于生成最小生成树,以便进行光照和阴影处理。
  3. 交通规划:在城市规划中优化道路和基础设施布局,以降低建设成本。

注意事项:

  • Prim算法适合于边相对较多的稠密图,对于稀疏图,Kruskal算法可能更为高效。
  • 确保输入的边权重非负,以确保生成树的正确性。

Prim算法是一种高效的生成树算法,通过选择权重最小的边来逐步构建最小生成树,广泛应用于网络设计、图形处理和交通规划等领域。

并查集

并查集(Union-Find)是一种用于处理不相交集合(Disjoint Set)的数据结构,广泛应用于图论、网络连通性、群体划分等问题。它支持两种基本操作:合并(Union)和查找(Find),并且通过优化可以达到接近常数时间的复杂度。

基本操作

  1. 查找(Find)

    • 用于确定某个元素属于哪个集合。通过跟随元素的父节点,找到根节点并返回其标识。
    • 为了提高效率,通常会进行路径压缩,即在查找过程中,将访问过的节点直接连接到根节点,从而减少未来的查找时间。
  2. 合并(Union)

    • 用于将两个集合合并成一个集合。首先查找两个元素的根节点,如果根节点不同,则将一个根节点的父节点指向另一个根节点。
    • 为了避免树的高度过高,通常会使用按秩合并(Union by Rank)策略,将较小的树合并到较大的树下。

数据结构

并查集通常使用一个数组来存储每个元素的父节点,以及一个额外的数组来记录每个树的秩(或大小):

  • parent数组parent[i]表示元素i的父节点。
  • rank数组rank[i]表示以元素i为根的树的高度。

并查集的实现示例

以下是并查集的Python实现示例:

python
class 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

算法复杂度

  • 查找操作:平均时间复杂度接近O(1)O(1),最坏情况下为O(logn)O(\log n),但由于路径压缩,实际应用中非常高效。
  • 合并操作:同样接近O(1)O(1)的平均时间复杂度,最坏情况下为O(logn)O(\log n)

应用场景

  1. 网络连通性:用于判断图中节点之间的连通性。
  2. 最小生成树:在Kruskal算法中用于判断边的连接是否形成环。
  3. 动态连通性问题:在处理动态添加边或节点的图时,用于高效查询连通性。
  4. 等价关系的划分:用于处理等价类的合并和查询。

优点与局限性

  • 优点:并查集通过路径压缩和按秩合并实现了高效的合并和查询,适用于多种问题。
  • 局限性:并查集适合处理静态的集合合并与查询,不适合频繁修改元素值的场景。

并查集是一种高效的集合管理数据结构,通过优化策略实现快速的查找和合并操作,广泛应用于计算机科学的多个领域。

结语

图论是离散数学的重要组成部分,拥有广泛的应用场景。从基本的图结构、表示方法到复杂的图算法,图论为解决现实问题提供了强有力的工具。在深入学习这些基础知识的同时,理解它们的实际应用将极大提升解决问题的能力。

通过上述介绍,读者可以更好地理解图论的基本概念,并通过例题巩固对图结构和算法的理解。如果你有任何问题或想法,欢迎在评论区讨论!

如果对你有用的话,可以打赏哦
打赏
ali pay
wechat pay

本文作者:Dong

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC。本作品采用《知识共享署名-非商业性使用 4.0 国际许可协议》进行许可。您可以在非商业用途下自由转载和修改,但必须注明出处并提供原作者链接。 许可协议。转载请注明出处!