从七桥问题到算法竞赛:图解Fleury与Hierholzer,谁才是寻找欧拉路径的更优解?

张开发
2026/4/21 17:45:36 15 分钟阅读

分享文章

从七桥问题到算法竞赛:图解Fleury与Hierholzer,谁才是寻找欧拉路径的更优解?
从七桥问题到算法竞赛图解Fleury与Hierholzer谁才是寻找欧拉路径的更优解18世纪普鲁士的哥尼斯堡城现俄罗斯加里宁格勒流传着一个有趣的谜题能否设计一条路线让人不重复地走过城中七座桥各一次数学家欧拉在1736年证明这是不可能的并由此开创了图论研究的先河。这个看似简单的谜题背后隐藏着计算机科学中一类重要问题——欧拉路径的求解。今天我们将从算法设计的角度深入探讨两种经典解法Fleury算法与Hierholzer算法。1. 欧拉图基础概念与判定1.1 欧拉迹与欧拉回路在图论中欧拉迹Eulerian trail是指经过图中每条边恰好一次的路径而欧拉回路Eulerian circuit则是起点和终点相同的欧拉迹。具有欧拉回路的图称为欧拉图只具有欧拉迹而无回路的图称为半欧拉图。欧拉在解决七桥问题时实际上证明了以下判定条件无向图欧拉回路判定连通无向图是欧拉图当且仅当所有顶点度数均为偶数无向图欧拉迹判定连通无向图具有欧拉迹当且仅当恰有两个顶点度数为奇数有向图欧拉回路判定强连通有向图是欧拉图当且仅当每个顶点入度等于出度提示在实际应用中DNA片段组装、电路板布线等问题都可以转化为欧拉路径问题。1.2 度数的计算与判断判断一个图是否为欧拉图首先需要计算每个顶点的度数。对于无向图度数就是与该顶点相连的边数对于有向图则需要分别计算入度和出度。# 计算无向图顶点度数的Python示例 def calculate_degrees(graph): degrees {} for vertex in graph: degrees[vertex] len(graph[vertex]) return degrees # 示例图键为顶点值为相邻顶点列表 sample_graph { A: [B, C], B: [A, C, D], C: [A, B, D], D: [B, C] } print(calculate_degrees(sample_graph)) # 输出{A: 2, B: 3, C: 3, D: 2}2. Fleury算法谨慎选择的艺术2.1 算法原理与步骤Fleury算法是最早提出的欧拉路径寻找方法之一其核心思想是逐步删除边同时避免走过桥即删除后会使图不连通的边除非没有其他选择。具体步骤如下检查图是否满足欧拉图或半欧拉图条件选择合适起点欧拉图任意顶点半欧拉图选择奇数度顶点选择一条边移动优先选择非桥边删除走过的边重复步骤3-4直到所有边被遍历2.2 时间复杂度分析Fleury算法的主要时间消耗在于每次选择边时都需要判断是否为桥。判断桥的标准方法是使用DFS或Tarjan算法查找割边这使得Fleury算法的时间复杂度达到O(E^2)其中E为边数。操作时间复杂度说明桥检测O(E)每次DFS遍历边选择O(E)最多选择E次总复杂度O(E^2)最坏情况下2.3 实际应用中的局限性虽然Fleury算法思路直观但在实际应用中存在明显缺陷桥检测开销大每次移动都需要进行连通性检查实现复杂需要维护图的连通性信息不适合大规模图平方级复杂度限制了其在大型网络中的应用# Fleury算法桥检测的简化实现 def is_bridge(graph, u, v): # 临时移除边 graph[u].remove(v) graph[v].remove(u) # 检查连通性 visited set() stack [u] while stack: node stack.pop() if node not in visited: visited.add(node) for neighbor in graph[node]: if neighbor not in visited: stack.append(neighbor) # 恢复边 graph[u].append(v) graph[v].append(u) return len(visited) ! len(graph) # 如果不连通则(u,v)是桥3. Hierholzer算法高效栈式解法3.1 算法核心思想Hierholzer算法采用完全不同的思路利用深度优先搜索(DFS)和栈结构来构建欧拉路径。其基本流程如下从合适起点开始进行DFS沿着边移动并删除已访问边当顶点没有未访问边时将其压入栈最后将栈中顶点逆序输出即为欧拉路径3.2 时间复杂度优势Hierholzer算法的优势在于其线性时间复杂度O(E)每个边只被访问一次。这使得它特别适合处理大规模图结构。算法步骤分解初始化选择起点欧拉图中任意点半欧拉图中奇数度点DFS遍历访问当前顶点的未访问边删除已访问边递归访问相邻顶点栈操作当顶点无未访问边时入栈最终逆序输出栈内容3.3 代码实现示例def hierholzer(graph): # 统计出度用于有向图 out_degree {u: len(graph[u]) for u in graph} # 选择起点这里简化处理实际应根据欧拉图条件选择 start next(iter(graph)) stack [start] path [] while stack: current stack[-1] if out_degree[current] 0: path.append(stack.pop()) else: next_node graph[current].pop() out_degree[current] - 1 stack.append(next_node) return path[::-1] # 逆序得到欧拉路径 # 有向欧拉图示例 directed_eulerian { A: [B], B: [C], C: [A] } print(hierholzer(directed_eulerian)) # 输出[A, B, C, A]4. 算法对比与应用场景4.1 性能对比特性Fleury算法Hierholzer算法时间复杂度O(E^2)O(E)空间复杂度O(VE)O(E)实现难度较高较低桥检测需要不需要适用性小规模图各种规模图4.2 实际应用选择在当代算法实践中Hierholzer算法几乎完全取代了Fleury算法主要原因包括效率优势线性时间复杂度的Hierholzer更适合现代大规模数据处理实现简洁不需要复杂的桥检测逻辑扩展性强易于适配有向图、加权图等变体4.3 应用案例DNA序列组装在生物信息学中DNA序列组装可以建模为欧拉路径问题将DNA片段分解为k-mers长度为k的子串构建de Bruijn图其中顶点代表(k-1)-mers边代表k-mers寻找欧拉路径即对应原始DNA序列# 简化的de Bruijn图构建 def build_de_bruijn(sequences, k): graph {} for seq in sequences: for i in range(len(seq) - k 1): kmer seq[i:ik] prefix kmer[:-1] suffix kmer[1:] if prefix not in graph: graph[prefix] [] graph[prefix].append(suffix) return graph # 示例DNA片段 dna_fragments [ATGCT, TGCTA, GCTAG, CTAGC] k 3 db_graph build_de_bruijn(dna_fragments, k) print(hierholzer(db_graph)) # 输出欧拉路径5. 算法竞赛中的优化技巧5.1 数据结构选择在算法竞赛中实现Hierholzer算法时数据结构的选择直接影响性能邻接表表示使用动态数组而非链表便于快速删除边栈实现用数组模拟栈比STL stack更快边删除记录指针或索引而非实际删除5.2 常见陷阱与调试起点选择错误半欧拉图必须从奇数度顶点开始图连通性检查确保图是连通的欧拉/半欧拉图边删除时机应在递归前立即删除避免重复访问5.3 性能优化示例// 高效的C Hierholzer实现 vectorint hierholzer(int start, vectorvectorint adj) { vectorint path; stackint st; st.push(start); while (!st.empty()) { int u st.top(); if (!adj[u].empty()) { int v adj[u].back(); adj[u].pop_back(); st.push(v); } else { path.push_back(u); st.pop(); } } reverse(path.begin(), path.end()); return path; }在解决实际编程竞赛问题时我通常会先快速检查图的欧拉性质然后直接应用优化后的Hierholzer实现。对于极端大规模数据还会考虑并行化处理或分治策略。

更多文章