本站内容由 AI 生成,可能存在错误。如发现问题,欢迎到 GitHub Issues 反馈。

欧拉与哈密顿:遍历的两种完备性

欧拉与哈密顿:遍历的两种完备性

更新于 2026-04-24

上一篇我们学习了 DAG 和拓扑排序——利用”无环”这一性质,让所有依赖都能被线性排序、让最短/最长路径都能在 O(V+E)O(V+E) 内求解。DAG 的核心是避免环。但如果图有环呢?如果我们不仅不回避环,还要追问:能不能走出一条完美的闭合回路——不多走一步、不少走一步?

这正是本文的问题。在 Part 1”探”的第四站,我们面对两个看起来非常相似的问题:

  1. 能不能不重复地走遍所有边?(欧拉路径/回路)
  2. 能不能不重复地走遍所有节点?(哈密顿路径/回路)

“边”和”节点”只差一个字。但这一个字的差别,将我们从多项式时间的优雅世界,带到 NP-complete 的深渊。这是你第一次直面 P vs NP。

算法范式:穷举搜索(Hierholzer 算法——系统地走遍所有边)、回溯(哈密顿暴力搜索——在排列空间中深度优先)

两种”走遍”:边 vs 节点

在正式定义之前,先直观感受这两种”遍历完备性”的差异。

欧拉路径 vs 哈密顿路径两种"遍历":走遍边 vs 走遍节点欧拉路径每条边恰好经过一次ABCDEB→A→E→D→C→B→E6 条边全走一遍,B 经过 2 次哈密顿路径每个节点恰好经过一次ABCDEA→B→C→D→E5 个节点全访问,边 B-E、A-C 未使用欧拉关注边的完整覆盖(节点可重复);哈密顿关注节点的完整覆盖(边可跳过)

欧拉路径(Euler path)要求经过图中每条边恰好一次——节点可以重复经过。如果路径的起点和终点相同,就叫欧拉回路(Euler circuit/tour)。

哈密顿路径(Hamiltonian path)要求经过图中每个节点恰好一次——某些边可以不被使用。如果路径首尾相连成环,就叫哈密顿回路(Hamiltonian cycle)。

用更精确的语言:

  • 欧拉路径:找一条路径 v0,e1,v1,e2,,em,vmv_0, e_1, v_1, e_2, \ldots, e_m, v_m,使得 EE 中每条边恰好出现一次
  • 哈密顿路径:找一条路径 v0,v1,,vn1v_0, v_1, \ldots, v_{n-1},使得 VV 中每个节点恰好出现一次
交互走查ABCDEF
[1/10]从 A 出发
已走边:0/9已访问节点:1/6
欧拉回路:走遍所有 9 条边,每条恰好一次,回到起点。节点可以重复经过(A 经过 3 次)。

切换上面的按钮,观察两种回路的走法——欧拉回路走完了所有 9 条边(节点 A 被重复经过了 3 次),而哈密顿回路走完了所有 6 个节点(内部 3 条边没有被使用)。

欧拉路径:图论的诞生

七桥问题——历史上第一个图论问题

1736 年,德国柯尼斯堡(Konigsberg)的居民在想一个问题:能不能从城中某个地方出发,恰好走过七座桥各一次,然后回到起点?

数学家 Leonhard Euler 将这个问题抽象为图:四块陆地是四个节点,七座桥是七条边。他证明了这是不可能的——不是通过穷举所有路线,而是通过一个优雅的度数论证

柯尼斯堡七桥:四块陆地、七座桥柯尼斯堡七桥问题 → 图模型1234567A北岸B西区C中岛D南岸deg(A)=3deg(B)=3deg(C)=5deg(D)=3四个节点度数全是奇数 → 奇度节点数 = 4 > 2 → 不存在欧拉路径 → 不可能不重复走遍七桥

Euler 的推理非常简单但深刻:如果存在一条从 ss 出发经过每条边恰好一次又回到 ss 的回路,那么每次”进入”一个节点(消耗一条边)就必须”离开”该节点(再消耗一条边)。因此每个节点消耗的边数是偶数——即每个节点的度数必须是偶数。柯尼斯堡的四个节点度数分别是 3, 3, 5, 3——全是奇数,所以不存在欧拉回路。甚至连欧拉路径都不存在(需要恰好 0 或 2 个奇度节点,这里有 4 个)。

这就是图论的诞生——Euler 不仅解决了这个具体问题,还开创了用图的结构性质(度数)回答存在性问题的方法论。

存在性条件——只需数度数

Euler 的推理直接给出了欧拉路径/回路存在的充要条件。对于连通图:

无向图

  • 欧拉回路存在 \Leftrightarrow 每个节点的度数 deg(v)\deg(v) 都是偶数(奇度节点数 = 0)
  • 欧拉路径存在 \Leftrightarrow 恰好有 0 个或 2 个奇度节点(若有 2 个,它们是路径的起点和终点)

有向图

  • 欧拉回路存在 \Leftrightarrow 每个节点的入度等于出度:deg+(v)=deg(v),vV\deg^+(v) = \deg^-(v), \forall v \in V
  • 欧拉路径存在 \Leftrightarrow 至多一个节点 deg+(v)=deg(v)+1\deg^+(v) = \deg^-(v) + 1(起点)、至多一个节点 deg(v)=deg+(v)+1\deg^-(v) = \deg^+(v) + 1(终点),其余节点入度等于出度
欧拉路径/回路存在性条件欧拉路径/回路的存在条件(无向图)欧拉回路 ✓所有节点度数为偶数Ad=3Bd=3Cd=3Dd=3Ed=2欧拉路径 ✓恰好 2 个奇度节点(起点和终点)Ad=2Bd=3Cd=2Dd=2Ed=3偶数度奇数度有向图条件:每个节点 入度 = 出度入=出=1入=出=1入=出=1入=出=1关键洞察:判定条件只需数度数 → O(V+E) 就能判定存在性

为什么这个条件是充分的(不仅仅是必要的)?直觉是:只要度数条件满足,你从任意节点出发沿未走过的边随意走,一定不会被困在中途——因为每个中间节点进出的边数相等,只有起点/终点可能不平衡。走到走不动的时候,你必然回到了起点(回路)或终点(路径)。如果此时还有未走过的边,它们一定构成若干个子回路,可以被拼接(splice)到主回路中——这正是 Hierholzer 算法的核心思想。

判定复杂度

判定欧拉路径/回路是否存在,只需遍历所有边统计度数:O(V+E)O(V + E)。这是线性时间——甚至不需要实际找出路径就能判定。

Hierholzer 算法——O(E)O(E) 构造欧拉回路

知道存在之后,如何构造这条回路?Hierholzer (1873) 给出了一个优雅的 O(E)O(E) 算法:

核心思想:不断走子回路,然后拼接成一条完整回路。

算法步骤

Hierholzer(G):
    选择任意节点 v₀(若找欧拉路径,选一个奇度节点)
    stack = [v₀]
    circuit = []
    while stack is not empty:
        v = stack.top()
        if v 有未使用的邻边 (v, u):
            将 (v, u) 标记为已使用
            stack.push(u)
        else:
            stack.pop()
            circuit.append(v)
    return circuit (reversed)

逐项理解:

  • stack:模拟 DFS 走当前路径——沿着未走过的边一直深入
  • 当栈顶节点没有未使用的边时,它被”弹出”并加入最终回路——这意味着从它出发的所有边都已走完
  • circuit:最终结果,存储的顺序是反向的(先弹出的是”最内层”的节点)

为什么是 O(E)O(E) 每条边被访问恰好一次(标记为已使用后不再访问),每个节点进出栈的总次数与它的度数成正比,所以总时间 O(E)O(E)

Hierholzer 算法步骤走查Hierholzer 算法走查 — 找欧拉回路示例图:5 个节点,8 条边,所有度数为偶数① 从 A 出发走回路ABCDEA→B→C→D→A② 找到有未用边的节点 BABCDEB 还有未用的边 → 从 B 开始新子回路③ 从 B 走新子回路ABCDEB→D→E→A→(→回到主路上的 B 处)④ 拼接:嵌入子回路ABCDEA→B→D→E→A→B→C→D→A所有边恰好走一次 ✓Hierholzer 算法核心思想1. 从任意节点出发,沿未走过的边走,直到回到起点 → 得到一个子回路2. 如果还有未走过的边,找到子回路上某个有未用边的节点3. 从该节点出发再走一个子回路4. 将新子回路嵌入(splice)到原回路中5. 重复直到所有边都走过 → 得到欧拉回路时间复杂度:O(E)每条边恰好被处理一次绿色粗线 = 当前步骤高亮的边 灰色虚线 = 已走过的边

数值走查:在上图的 5 节点 8 边图上,Hierholzer 的执行过程是:

  1. 从 A 出发,走 A→B→C→D→A,回到起点。这是一个子回路,消耗了 4 条边。
  2. 检查子回路上的节点——B 还有未使用的边(B-D 和 B-E)。
  3. 从 B 出发走新子回路:B→D→E→A→B,消耗剩余 4 条边。
  4. 将子回路嵌入主回路的 B 处:A→B→D→E→A→B→C→D→A。
  5. 所有 8 条边都走过了,得到完整的欧拉回路。

哈密顿路径:从优雅到深渊

问题定义

哈密顿路径问题看起来和欧拉路径”差不多”——只是把”每条边走一次”换成”每个节点走一次”。但计算复杂性的差别是天壤之别。

哈密顿路径(Hamiltonian path)问题:给定图 G=(V,E)G = (V, E),是否存在一条简单路径(不重复节点)恰好经过每个节点一次?

哈密顿回路(Hamiltonian cycle)问题:是否存在一个简单环恰好经过每个节点一次?

与欧拉问题的关键区别:

  • 欧拉问题有简单的充要条件(度数奇偶性)→ O(V+E)O(V+E) 判定
  • 哈密顿问题没有已知的简单充要条件 → 没有已知的多项式时间判定算法

NP-completeness

Karp (1972) 在其里程碑式的论文中证明了哈密顿回路问题是 NP-complete 的。这意味着:

  1. NP 中:如果有人给你一条候选路径,你可以在 O(n)O(n) 时间内验证它是否是哈密顿路径(检查是否每个节点恰好出现一次)
  2. NP-hard:如果你能在多项式时间内求解哈密顿回路,那你就能在多项式时间内求解所有 NP 问题——包括 SAT、图着色、子集和等上千个问题

目前人类已知的最好精确算法是动态规划(Held-Karp 算法),时间复杂度 O(n22n)O(n^2 \cdot 2^n)——比暴力的 O(n!)O(n!) 好,但仍然是指数级的。

暴力搜索与回溯

最直观的算法是回溯法:尝试所有可能的节点排列,检查每对相邻节点之间是否有边。

HamiltonianDFS(G, path, visited):
    if |path| = n:
        if (path[n-1], path[0]) ∈ E:  // 检查能否成环(回路版本)
            return path
        return null
    v = path.last()
    for each u ∈ N(v):
        if u ∉ visited:
            visited.add(u)
            path.append(u)
            result = HamiltonianDFS(G, path, visited)
            if result ≠ null: return result
            path.removeLast()
            visited.remove(u)
    return null
哈密顿路径:回溯搜索哈密顿路径搜索 — 回溯法原图 (5 节点)ABCDE解:A→B→C→D→E搜索树(部分展示)ABCDCEDE找到!最坏情况:O(n!) 种排列都要检查 — NP-complete 问题即使只有 20 个节点:20! ≈ 2.4 × 10¹⁸ 剪枝能减少但不能改变指数级本质

最坏情况下,搜索树有 O(n!)O(n!) 个叶子(nn 个节点的所有排列)。剪枝可以在实际中大大减少搜索量——比如当某个未访问节点已经被”隔离”(没有与当前路径连接的未访问邻居)时,可以提前回溯。但从理论上说,没有任何已知的剪枝策略能将最坏复杂度降到多项式

一些充分条件(但不是充要条件)

虽然没有完美的判定条件,数学家发现了一些充分条件——如果图足够”稠密”,哈密顿回路几乎一定存在:

Dirac 定理 (1952):如果 n3n \geq 3 且每个节点的度数 deg(v)n/2\deg(v) \geq n/2,则 GG 有哈密顿回路。

Ore 定理 (1960):如果 n3n \geq 3 且对于每对不相邻节点 u,vu, v,都有 deg(u)+deg(v)n\deg(u) + \deg(v) \geq n,则 GG 有哈密顿回路。

这些条件本质上是说:图足够稠密(边足够多)时,哈密顿回路一定存在。但它们只是充分条件——有些稀疏图也有哈密顿回路(如环图 CnC_n),而满足条件的图的判定反而不是难点。真正难的是一般图。

P vs NP 的震撼对比

现在让我们把两个问题放在一起,直面这个计算复杂性中最令人震撼的对比:

P vs NP 的震撼对比一字之差,天壤之别"走遍所有边" vs "走遍所有节点"欧拉问题(边)问题经过每条边恰好一次判定数度数 → O(V+E)构造Hierholzer → O(E)复杂度类P(多项式时间)1736年Euler 完全解决哈密顿问题(节点)问题经过每个节点恰好一次判定无已知多项式算法构造回溯 → O(n!)复杂度类NP-complete至今仍未找到高效算法vs这是计算复杂性理论中最令人震撼的对比之一:看似几乎相同的两个问题,一个 O(E) 就能解决,另一个是人类已知最难的问题类之一

两个问题的数学描述几乎一样——“走遍所有 X 恰好一次”,只是 X 从”边”变成了”节点”。但:

  • 欧拉问题:1736 年 Euler 给出完美解决方案,O(V+E)O(V+E) 判定,O(E)O(E) 构造
  • 哈密顿问题:288 年后的今天(2026 年),仍然没有多项式算法,被证明是 NP-complete

这不是”还没找到好算法”的问题。NP-completeness 意味着,除非 P = NP(绝大多数计算机科学家认为不太可能),哈密顿回路就不可能有多项式时间算法。

为什么一个如此容易,一个如此难? 直觉上:

  • 欧拉条件是局部的——每个节点只需检查自己的度数,不需要全局信息
  • 哈密顿条件是全局的——你必须考虑节点之间的所有可能路径,这些路径的数量是指数级的

哈密顿与 TSP:加上权重,就是旅行商

哈密顿回路还有一个更著名的”升级版”——旅行商问题(Traveling Salesman Problem, TSP)。

TSP = 最短哈密顿回路 + 权重哈密顿回路 → TSP:加上权重,就是旅行商哈密顿回路"存在经过每个节点一次的回路吗?"ABCDE判定问题:NP-complete+ 权重+ "找最短"旅行商问题 (TSP)"经过每个城市一次的最短回路是什么?"37254ABCDE优化问题:NP-hard总权重 = 21

TSP 的定义:给定一个完全图(每对节点之间都有边),每条边有权重 w(u,v)w(u,v)(代表距离/成本),找一条权重和最小的哈密顿回路。

TSP 和哈密顿回路的关系:

  • 哈密顿回路是 TSP 的判定版本:“权重和 W\leq W 的哈密顿回路是否存在?”
  • TSP 是哈密顿回路的优化版本:“在所有哈密顿回路中找权重和最小的”
  • 因此 TSP 至少和哈密顿回路一样难——TSP 是 NP-hard

TSP 是组合优化中最经典的问题之一。我们将在 Art. 15: NP-hard 与近似算法 中详细讨论 TSP 的近似算法。在这里只需要知道:哈密顿回路是 TSP 的核心子问题

应用场景

DNA 测序组装——欧拉路径的杀手级应用

现代 DNA 测序技术(如 Illumina 短读段测序)将基因组打碎成数百万个短片段(reads),每个片段长度约 100-300 个碱基对。组装问题就是:如何从这些重叠的短片段还原出完整的基因组序列?

Pevzner 等人 (2001) 提出了一个优雅的解决方案——将问题转化为 de Bruijn 图上的欧拉路径问题:

  1. 选择参数 kk(k-mer 长度)
  2. 从每个 read 中提取所有长度为 kk 的子串(k-mer)
  3. 构建 de Bruijn 图
    • 节点 = 所有长度为 k1k-1 的子串((k-1)-mer)
    • 有向边 = 每个 k-mer(从它的前 k1k-1 个字符指向后 k1k-1 个字符)
  4. 在这个有向图上找欧拉路径——走遍所有边恰好一次 = 使用了所有 k-mer = 还原了完整序列
DNA 测序:de Bruijn 图上的欧拉路径DNA 测序 — de Bruijn 图上的欧拉路径将 DNA 短读段(k-mer)转为图:节点 = (k-1)-mer,边 = k-merATGTGCGCACATATTGGCCA为什么用欧拉路径而非哈密顿路径?• 节点 = (k-1)-mer → 数量 = O(4^(k-1)),可管理• 边 = k-mer = 一个短读段 → 走遍所有边 = 用完所有读段• 欧拉路径 O(E) 即可求解 → 这正是现代测序组装软件的核心

为什么不用哈密顿路径?因为如果以 k-mer 为节点构图(overlap graph),组装问题就变成了找哈密顿路径——NP-complete!转换为 de Bruijn 图后,k-mer 变成了,组装问题变成了找欧拉路径——O(E)O(E)同一个问题,换一种建模方式,从 NP-complete 变成了线性时间。 这正是图论建模思维的威力。

这一思想被 Velvet、SPAdes 等主流基因组组装工具采用,是现代基因组学的基石。

中国邮递员问题——欧拉路径的变体

“一个邮递员要走遍所在地区的每一条街道投递信件,他从邮局出发,投递完所有信件后回到邮局。怎样走使得总路程最短?” 这个问题由中国数学家管梅谷于 1962 年提出。

  • 如果图恰好有欧拉回路(所有节点度数为偶数),答案就是欧拉回路——每条边走一次,没有浪费
  • 如果有奇度节点,需要重复走某些边来”修复”度数的奇偶性。问题变成:用最小成本添加重复边,使所有节点度数变为偶数

对无向图,中国邮递员问题可以在多项式时间内求解(通过最小权完美匹配将奇度节点两两配对)。但对有向图和混合图,问题变成 NP-hard。

实际应用:除雪车路线规划、垃圾收集车路线、电路板检测设备的运动路径。

物流与制造——哈密顿问题的工程实例

  • 物流路径规划:快递公司每天要配送数百个包裹到不同地址——这就是 TSP/VRP(Vehicle Routing Problem)。现实中无法精确求解大规模 TSP,使用各种近似和启发式算法(2-opt、LKH、OR-Tools)
  • 芯片制造:PCB 钻孔机需要依次访问电路板上所有钻孔点,最小化总移动距离——这是 TSP 的一个实例
  • 望远镜调度:天文望远镜需要在一个晚上观测多个天体目标,最小化调整方向的总时间——也是 TSP 变体
应用场景一览应用场景一览欧拉类(多项式时间)哈密顿类(NP-hard)欧拉路径DNA 测序组装de Bruijn 图上走遍所有读段O(E)中国邮递员问题电路板检测走遍所有焊点连线(欧拉路径变体)O(E)中国邮递员问题除雪/垃圾收集走遍所有街道最小化重复距离O(E)TSP / 车辆路径物流路径规划访问所有城市最小化总距离NP-hard哈密顿路径芯片制造激光/钻头依次访问所有点NP-hard绿色 = 有高效精确解 红色 = 只能近似或对小规模精确求解

复杂度对比表

问题判定复杂度(最坏)构造复杂度(最坏)复杂度类关键条件/算法
欧拉路径/回路(无向图)O(V+E)O(V+E)O(E)O(E)P奇度节点数 {0,2}\in \{0, 2\} / Hierholzer
欧拉路径/回路(有向图)O(V+E)O(V+E)O(E)O(E)Pdeg+=deg\deg^+ = \deg^- / Hierholzer
哈密顿路径/回路O(n22n)O(n^2 \cdot 2^n)*O(n22n)O(n^2 \cdot 2^n)*NP-completeHeld-Karp DP / 回溯
TSP(最短哈密顿回路)O(n22n)O(n^2 \cdot 2^n)*NP-hardHeld-Karp DP / 近似算法
中国邮递员(无向)O(V+E)O(V+E)O(V3)O(V^3)P最小权完美匹配

*Held-Karp 是已知最优精确算法,仍为指数级

⚙️ 迭代机器视角Hierholzer (Euler circuit)

Hierholzer 算法是构造性算法——通过拼接回路段来构建 Euler 回路,不是在求解方程或优化问题。属于 COMP(组合构造)范式。

→ 框架详解:Art. 0A

🔁 迭代机器视角Hamilton path (backtracking)CST: 约束满足

Graph隐式状态空间树
State[v]当前路径 + visited 集合
SELECTLIFO(DFS 回溯)
COMBINE尝试扩展路径 → 检查约束
重入可回退
TERMINATE找到解或穷尽
求解方法B&P

→ 框架详解:Art. 0A — 图上的通用迭代机器

总结

本文展示了图论中最令人震撼的对比之一——两个”看起来几乎一样”的问题,计算复杂度却天差地别:

  • 欧拉路径/回路:走遍每条边恰好一次。判定条件极其简单(数度数),构造算法极其高效(Hierholzer, O(E)O(E))。这是图论的起源——1736 年 Euler 解决七桥问题时开创了整个学科
  • 哈密顿路径/回路:走遍每个节点恰好一次。NP-complete,288 年后仍无多项式算法。这是计算复杂性的核心问题之一
  • P vs NP 的第一次照面:一字之差(边 vs 节点),但前者在 P 中,后者在 NP-complete 中。这个差距提醒我们:问题的难度不在于它的描述有多复杂,而在于解空间的结构
  • 应用:欧拉路径驱动了 DNA 测序组装和路线规划;哈密顿问题(TSP)是物流、制造、调度的核心

DAG(上一篇)保证了无环性,让线性时间算法成为可能。欧拉/哈密顿(本篇)追求闭合回路上的完备遍历。但图中还有一类极其重要的特殊结构——。树是连通无环图,它同时满足”连通”和”无环”两个性质,比 DAG 更特殊。下一篇 Art. 5: 树上算法 将展示:在树上,许多在一般图上困难的问题都有优雅高效的解——因为树没有环、没有多余的路径,它是图的”最经济”连通形式。

实现指引

算法函数/方法
欧拉回路判定NetworkXnx.is_eulerian(G)
欧拉回路构造NetworkXnx.eulerian_circuit(G)
欧拉路径构造NetworkXnx.eulerian_path(G)
有向欧拉回路NetworkXnx.is_eulerian(G) (DiGraph)
哈密顿路径NetworkX无内置——需自行实现回溯或 DP
TSP 近似NetworkXnx.approximation.traveling_salesman_problem(G)
TSP 精确/启发式Google OR-Toolsortools.constraint_solver.routing
中国邮递员NetworkXnx.eulerian_circuit(nx.eulerize(G))
欧拉回路igraph无直接内置——可通过 DFS 自行实现