欧拉路径问题源于著名的柯尼斯堡七桥问题,是图论中最经典的问题之一。给定一个图,判断是否存在一条经过每条边恰好一次的路径(欧拉路径),或者一条经过每条边恰好一次并回到起点的回路(欧拉回路)。该问题在路径规划、电路板布线、DNA序列组装、游戏关卡设计等领域有广泛应用。
问题定义
- 欧拉回路:从某节点出发,经过图中每条边恰好一次,最终回到起点的闭合路径
- 欧拉路径:从某节点出发,经过图中每条边恰好一次的路径(不要求回到起点)
存在性判定定理
无向图:
– 欧拉回路:所有顶点度数为偶数,且图连通
– 欧拉路径:恰好有0个或2个顶点度数为奇数,且图连通
有向图:
– 欧拉回路:所有顶点入度等于出度,且图连通
– 欧拉路径:恰好一个顶点出度 = 入度 + 1(起点),一个顶点入度 = 出度 + 1(终点),其余入度等于出度
方法一:Hierholzer算法(O(V + E))
核心思路
Hierholzer算法的核心思想是:从起点出发,沿着未访问的边随意走,直到无法继续前进。此时如果还有未访问的边,说明当前路径上存在”环”可以扩展。回溯到路径上第一个还有未访问边的节点,从该点继续探索,将新路径插入到原路径中。
Java实现(无向图)
import java.util.*;
public class EulerPathUndirected {
private int n;
private List<int[]>[] graph; // 存储[邻接点, 边编号]
private boolean[] used; // 边是否被访问
private int edgeCount;
@SuppressWarnings("unchecked")
public EulerPathUndirected(int n, int m) {
this.n = n;
this.graph = new ArrayList[n];
this.used = new boolean[m];
this.edgeCount = 0;
for (int i = 0; i < n; i++) {
graph[i] = new ArrayList<>();
}
}
/**
* 添加无向边
*/
public void addEdge(int u, int v) {
graph[u].add(new int[]{v, edgeCount});
graph[v].add(new int[]{u, edgeCount});
edgeCount++;
}
/**
* 判断是否存在欧拉路径/回路
* @return [是否存在, 起点]
*/
public Object[] checkExistence() {
int oddDegreeCount = 0;
int start = 0;
for (int i = 0; i < n; i++) {
if (graph[i].size() % 2 == 1) {
oddDegreeCount++;
start = i; // 记录奇度顶点作为起点
}
}
// 0个奇度顶点:欧拉回路;2个奇度顶点:欧拉路径
boolean exists = (oddDegreeCount == 0 || oddDegreeCount == 2);
if (oddDegreeCount == 0) {
// 找第一个有边的节点作为起点
for (int i = 0; i < n; i++) {
if (!graph[i].isEmpty()) {
start = i;
break;
}
}
}
return new Object[]{exists, start};
}
/**
* Hierholzer算法求欧拉路径
* @return 欧拉路径的节点序列,如果不存在则返回空列表
*/
public List<Integer> findEulerPath() {
Object[] check = checkExistence();
boolean exists = (Boolean) check[0];
int start = (Integer) check[1];
if (!exists) {
return new ArrayList<>();
}
List<Integer> path = new ArrayList<>();
Stack<Integer> stack = new Stack<>();
stack.push(start);
while (!stack.isEmpty()) {
int u = stack.peek();
boolean found = false;
// 找一条未使用的边
for (int i = 0; i < graph[u].size(); i++) {
int[] edge = graph[u].get(i);
int v = edge[0];
int eid = edge[1];
if (!used[eid]) {
used[eid] = true;
stack.push(v);
found = true;
break;
}
}
if (!found) {
// 没有未使用的边,将节点加入路径
path.add(stack.pop());
}
}
// 路径是逆序的,需要反转
Collections.reverse(path);
return path;
}
public static void main(String[] args) {
// 构建示例图(柯尼斯堡七桥问题的简化版)
// 0 -- 1 -- 2
// | | |
// 3 -- 4 -- 5
EulerPathUndirected euler = new EulerPathUndirected(6, 7);
euler.addEdge(0, 1);
euler.addEdge(1, 2);
euler.addEdge(0, 3);
euler.addEdge(1, 4);
euler.addEdge(2, 5);
euler.addEdge(3, 4);
euler.addEdge(4, 5);
List<Integer> path = euler.findEulerPath();
System.out.println("欧拉路径: " + path);
System.out.println("路径长度(边数): " + (path.size() - 1));
}
}
方法二:有向图欧拉路径
Java实现
import java.util.*;
public class EulerPathDirected {
private int n;
private List<Integer>[] graph; // 邻接表
private int[] inDegree;
private int[] outDegree;
@SuppressWarnings("unchecked")
public EulerPathDirected(int n) {
this.n = n;
this.graph = new ArrayList[n];
this.inDegree = new int[n];
this.outDegree = new int[n];
for (int i = 0; i < n; i++) {
graph[i] = new ArrayList<>();
}
}
public void addEdge(int from, int to) {
graph[from].add(to);
outDegree[from]++;
inDegree[to]++;
}
/**
* 判断是否存在欧拉路径/回路
* @return [是否存在, 起点]
*/
public Object[] checkExistence() {
int startNodes = 0; // 出度 = 入度 + 1
int endNodes = 0; // 入度 = 出度 + 1
int start = 0;
for (int i = 0; i < n; i++) {
int diff = outDegree[i] - inDegree[i];
if (Math.abs(diff) > 1) {
return new Object[]{false, -1};
}
if (diff == 1) {
startNodes++;
start = i;
} else if (diff == -1) {
endNodes++;
}
}
boolean exists = (startNodes == 0 && endNodes == 0) ||
(startNodes == 1 && endNodes == 1);
if (startNodes == 0) {
// 欧拉回路,找第一个有出边的节点
for (int i = 0; i < n; i++) {
if (outDegree[i] > 0) {
start = i;
break;
}
}
}
return new Object[]{exists, start};
}
/**
* Hierholzer算法求有向图欧拉路径
*/
public List<Integer> findEulerPath() {
Object[] check = checkExistence();
boolean exists = (Boolean) check[0];
int start = (Integer) check[1];
if (!exists) {
return new ArrayList<>();
}
// 复制邻接表用于删除边
List<Integer>[] tempGraph = new ArrayList[n];
for (int i = 0; i < n; i++) {
tempGraph[i] = new ArrayList<>(graph[i]);
}
List<Integer> path = new ArrayList<>();
Stack<Integer> stack = new Stack<>();
stack.push(start);
while (!stack.isEmpty()) {
int u = stack.peek();
if (!tempGraph[u].isEmpty()) {
// 还有未访问的边,继续走
int v = tempGraph[u].remove(tempGraph[u].size() - 1);
stack.push(v);
} else {
// 没有未访问的边,加入路径
path.add(stack.pop());
}
}
Collections.reverse(path);
return path;
}
public static void main(String[] args) {
// 构建有向图示例
// 0 -> 1 -> 2 -> 0
// 1 -> 3 -> 1
EulerPathDirected euler = new EulerPathDirected(4);
euler.addEdge(0, 1);
euler.addEdge(1, 2);
euler.addEdge(2, 0);
euler.addEdge(1, 3);
euler.addEdge(3, 1);
List<Integer> path = euler.findEulerPath();
System.out.println("有向图欧拉路径: " + path);
}
}
方法三:字典序最小欧拉路径
当图中存在多个欧拉路径时,有时需要输出字典序最小的路径。通过将邻接表排序,并在Hierholzer算法中优先选择编号较小的邻居,可以实现这一目标。
Java实现
import java.util.*;
public class EulerPathLexicographically {
private int n;
private PriorityQueue<Integer>[] graph; // 用小根堆维护邻接点
@SuppressWarnings("unchecked")
public EulerPathLexicographically(int n) {
this.n = n;
this.graph = new PriorityQueue[n];
for (int i = 0; i < n; i++) {
graph[i] = new PriorityQueue<>();
}
}
public void addEdge(int u, int v) {
graph[u].offer(v);
}
/**
* 求字典序最小的欧拉路径(有向图)
*/
public List<Integer> findLexicographicallySmallest(int start) {
List<Integer> path = new ArrayList<>();
Stack<Integer> stack = new Stack<>();
stack.push(start);
while (!stack.isEmpty()) {
int u = stack.peek();
if (!graph[u].isEmpty()) {
int v = graph[u].poll();
stack.push(v);
} else {
path.add(stack.pop());
}
}
Collections.reverse(path);
return path;
}
public static void main(String[] args) {
EulerPathLexicographically euler = new EulerPathLexicographically(5);
euler.addEdge(0, 1);
euler.addEdge(0, 2);
euler.addEdge(1, 3);
euler.addEdge(2, 3);
euler.addEdge(3, 0);
euler.addEdge(3, 4);
euler.addEdge(4, 3);
List<Integer> path = euler.findLexicographicallySmallest(0);
System.out.println("字典序最小欧拉路径: " + path);
}
}
复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 存在性判定 | O(V + E) | O(V) |
| Hierholzer求路径 | O(V + E) | O(V + E) |
| 字典序最小路径 | O(E log E) | O(V + E) |
实际应用场景
- DNA序列组装:将片段重叠图分解为欧拉路径,重建完整序列
- 电路板布线:寻找经过所有连接点的最优路径
- 邮递员问题:中国邮路问题的基础,寻找经过所有街道的最短路径
- 游戏关卡设计:设计遍历所有区域的最优路线
- 路线规划:规划经过所有景点的最短游览路线
变种问题
半欧拉路径(非欧拉但接近)
当图不存在欧拉路径时,可以通过添加最少的边或重复经过某些边来实现遍历。这引申出中国邮路问题,可用最小权匹配求解。
public class ChinesePostman {
/**
* 中国邮路问题:求经过每条边至少一次的最短路径
* 核心思想:对奇度顶点求最小权完美匹配
*/
public int chinesePostman(int n, int[][] edges) {
// 实现略,核心步骤:
// 1. 找出所有奇度顶点
// 2. 计算奇度顶点之间的最短路径(Floyd或Dijkstra)
// 3. 对奇度顶点求最小权完美匹配
// 4. 原图总权值 + 匹配增加的权值
return 0;
}
}
总结
欧拉路径问题是图论的经典入门,Hierholzer算法以线性时间优雅地解决了路径构造问题。理解欧拉路径的存在性判定和构造方法,不仅是算法竞赛的基础,也是解决实际网络遍历、路径规划问题的有力工具。从柯尼斯堡七桥到现代DNA测序,欧拉路径的思想跨越三百多年依然闪耀。