欧拉回路/路径
先讲一下定义:
-
欧拉路径是一个图上从一个点到另一个点的路径,使得每一条边都被走过了。
-
欧拉回路是一个图上从一个点回到自己的环,使得每一条边都被走过了。
我们不妨先看一下这道题:T415964。也就是边的深搜。
这种方法实际上没啥用,但是有助于对后面的欧拉路径做铺垫。
容易发现,边的深搜也应该是“不撞墙不回头”的,即只有当当前的结点没有任何没有走过的边的时候,才能回溯。
为了便于理解,这里不妨举个例子:
为了判断那些边被走过了那些边没被走过,不妨使用一个 cur[i] 表示点 \(i\) 当前的最早的没有走过的边的编号(即这条边对应的点为 v[i][cur[i]])。
这里的 cur 思想,就是以后欧拉路径的一种思想。
实际上,欧拉路径/回路可以视为整个图论的起源。
有一个典型的例子就是哥尼斯堡七桥问题,河岸已经被抽象成了点,桥已经被抽象成了边。

发现无论怎么走,都没法将所有的边走恰好一遍。在后面确实被证明是无法走完的。
这是一个数学故事,也是欧拉路径/回路的来源。
不如将图分为无向图和有向图来分别考虑。
无向图
我们小学奥数学过一笔画问题,这可以用来判断一个无向图是否存在欧拉路径/回路。
首先,图必须联通。
使用以下一笔画问题的结论,就可以判断是否存在欧拉路径/回路:
-
如果度数为奇数的点有恰好 \(0\) 个,则有欧拉回路。
-
如果度数为奇数的点有恰好 \(2\) 个,则有欧拉路径。
- 而且欧拉路径一定是从一个度数为奇数的点出发,到达另一个度数为奇数的点。
-
如果上面两个条件都不满足,则不存在欧拉路径/回路。
有向图
第一点:如果所有的点的入度和出度都是相等的,则该有向图存在欧拉回路。
因为每一个点经过一次都必须是一个一进一出,而且这是回路,入度和出度都相等才行。
显然可以得到,如果是欧拉路径的话,对入度和出度的关系就没有那么高了。因为起始点只需要是一出,终点只需要是一进即可。
第二点:如果所有的点中,入度比出度多 \(1\) 的点有恰好一个,出度比入度多 \(1\) 的点也恰好有一个,其他相等:则该有向图存在欧拉路径。
这里的原因在上面已经讲过了。
注意,欧拉路径的起始点是出度比入度多 \(1\) 的点,终点是入度比出度多 \(1\) 的点。
第三点:如果前面两个都不满足,则该有向图不存在欧拉路径/回路。
显然。
这样就可以判断完毕了。
算法思路
可以看成,无向图和有向图的判断路径/回路的方法虽然不一样,但是找路径/回路的过程是一模一样的。
算法的思想很简单:先确定一个路径/回路,然后尝试在基础上扩展路径。
我们举一个很简单的例子:奥运五环。

可以证明,里面一定存在一个欧拉路径。
我们先从最左边的点出发,一不小心,走掉了第一个环回到了自己。但是实际上还有四个环没走。
于是考虑扩张路径。注意到第一个环和第二个环存在交点,于是考虑从这个交点扩张。又不小心,又走掉了第二个环。至少,路径扩张成功了一部分。
于是就这么扩张即可。最终得到了答案为:
注意这里的实现使用的是深搜。
是不是有一点边深搜的感觉了?举个例子。

假设从 \(1\) 号结点开始。走过的边不能再走一遍。
一号结点不小心走了 \(1 \to 2 \to 3 \to 4 \to 5 \to 1\) 回到了自己。
然后就是经典边深搜:\(1\) 已经无路可走,因为 \(5\) 也无路可走,于是回溯:\(1 \to 5 \to 4\)。
发现 \(4\) 有路了!
假设一路走 \(4 \to 3 \to 7 \to 6 \to 8 \to 4\) 回到了自己。
然后发现 \(4\) 无路可走。回溯到 \(8\)。发现 \(8\) 有路了。
然后 \(8 \to 6 \to 7 \to 8\)。所有的边都走完了,然后就一路回溯到 \(1\)。
但是我们会发现有一些难以统计路径:在搜索中,\(1 \to 2 \to 3 \to 4 \to 5 \to 1\) 构成一个环,\(4 \to 3 \to 7 \to 6 \to 8 \to 4\) 构成一个环,\(8 \to 6 \to 7 \to 8\) 应该构成一个环。但是,当已经有 \(1 \to 2 \to 3 \to 4\) 的时候就已经需要跳到 \(4 \to 3 \to 7 \to 6 \to 8 \to 4\) 这个环了。
于是考虑常见套路:在每一个点回溯的时候再记录下这个点的编号。注意,这样得到的是欧拉路径的反路径。
可以自己模拟一下图的深搜过程,最终得到的应该是 \(1,5,4,9,6,7,9,6,7,3,4,3,2,1\),反转过来正好就是符合要求的欧拉回路(也可以称作欧拉路径)。
于是就可以得到答案序列 \(1 \to 2 \to 3 \to 4 \to 3 \to 7 \to 6 \to 8 \to 6 \to 7 \to 8 \to 4 \to 5 \to 1\)。
代码其实很好写。
实际上我们可以使用栈来存储编号。
模板代码
无向图的代码:
//模板题:P2731
#include <bits/stdc++.h>
using namespace std;
int n = 500, m;
const int N = 2010;
int dis[N], cur[N]; // dis 用于记录每个顶点的度数,cur 用于记录当前顶点的邻接边遍历位置
vector<pair<int, int> > v[N]; // 邻接表
stack<int> stk; // 用于存储欧拉路径的栈
bool f[N]; // 标记边是否被访问过
// Hierholzer 算法实现,用于寻找欧拉路径/回路
void hierholzer(int u) {
// 遍历当前顶点的所有邻接边
for (int &i = cur[u]; i < (int)v[u].size();) {
if (f[v[u][i].second]) { // 如果边已经被访问过,跳过
i++;
continue;
}
f[v[u][i].second] = 1; // 标记边为已访问
hierholzer(v[u][i++].first); // 递归访问邻接顶点
}
stk.push(u); // 将当前顶点压入栈中
}
int mi = 1e9; // 记录图中最小顶点的编号
// 判断是否存在欧拉路径/回路,并输出结果
bool euler() {
// 对每个顶点的邻接表进行排序,确保按顶点编号从小到大遍历
for (int i = 1; i <= n; i++)
sort(v[i].begin(), v[i].end());
int st = 1e9, cnt = 0; // st 是欧拉路径的起点,cnt 是奇数度顶点的数量
// 统计奇数度顶点的数量,并找到最小的奇数度顶点作为起点
for (int i = 1; i <= n; i++)
if (dis[i] % 2)
cnt++, st = min(st, i);
if (!cnt) // 如果没有奇数度顶点,说明存在欧拉回路,起点为最小顶点
st = mi;
// 如果奇数度顶点的数量为 0 或 2,说明存在欧拉路径/回路
if (cnt == 2 || cnt == 0) {
hierholzer(st); // 调用 Hierholzer 算法寻找欧拉路径/回路
while (!stk.empty()) // 输出欧拉路径/回路
cout << stk.top() << endl, stk.pop();
return 1;
}
return 0; // 否则,不存在欧拉路径/回路
}
int main() {
cin >> m; // 输入边的数量
for (int i = 1; i <= m; i++) {
int x, y;
cin >> x >> y; // 输入边的两个顶点
v[x].push_back({y, i}); // 将边加入邻接表
v[y].push_back({x, i});
mi = min(mi, x), mi = min(mi, y); // 更新最小顶点编号
dis[y]++, dis[x]++; // 更新顶点的度数
}
if (!euler()) // 调用 euler 函数判断是否存在欧拉路径/回路
cout << "No"; // 如果不存在,输出 "No"
return 0;
}
有向图的代码:
//模板题:P7771
#include <bits/stdc++.h>
using namespace std;
int n, m;
const int N = 100010;
int dis[N], pos[N]; // dis 用于记录每个顶点的入度与出度的差值,pos 用于记录当前顶点的邻接边遍历位置
vector<int> v[N]; // 邻接表
stack<int> stk; // 用于存储欧拉路径的栈
// Hierholzer 算法实现,用于寻找欧拉路径/回路
void hierholzer(int u) {
// 遍历当前顶点的所有邻接边
for (int &i = pos[u]; i < (int)v[u].size();)
hierholzer(v[u][i++]); // 递归访问邻接顶点
stk.push(u); // 将当前顶点压入栈中
}
// 判断是否存在欧拉路径/回路,并输出结果
bool euler() {
// 对每个顶点的邻接表进行排序,确保按顶点编号从小到大遍历
for (int i = 1; i <= n; i++)
sort(v[i].begin(), v[i].end());
int st = 1, cnt0 = 0, cnt1 = 0; // st 是欧拉路径的起点,cnt0 是入度比出度多1的顶点数量,cnt1 是出度比入度多1的顶点数量
// 统计入度与出度的差值,并找到起点
for (int i = 1; i <= n; i++)
if (dis[i] == 1) // 入度比出度多1,说明是欧拉路径的终点
st = i, cnt0++;
else if (dis[i] == -1) // 出度比入度多1,说明是欧拉路径的起点
cnt1++;
else if (dis[i] != 0) // 如果存在其他情况,说明不存在欧拉路径/回路
return 0;
// 如果满足以下条件之一,说明存在欧拉路径/回路:
// 1. 所有顶点的入度等于出度(欧拉回路)
// 2. 只有一个顶点的入度比出度多1,且只有一个顶点的出度比入度多1(欧拉路径)
if ((!cnt0 && !cnt1) || (cnt0 == 1 && cnt1 == 1)) {
hierholzer(st); // 调用 Hierholzer 算法寻找欧拉路径/回路
while (!stk.empty()) // 输出欧拉路径/回路
cout << stk.top() << " ", stk.pop();
return 1;
}
return 0; // 否则,不存在欧拉路径/回路
}
int main() {
cin >> n >> m; // 输入顶点数量 n 和边数量 m
for (int i = 1; i <= m; i++) {
int x, y;
cin >> x >> y; // 输入边的两个顶点
v[x].push_back(y); // 将边加入邻接表
dis[y]--, dis[x]++; // 更新顶点的入度与出度的差值
}
if (!euler()) // 调用 euler 函数判断是否存在欧拉路径/回路
cout << "No"; // 如果不存在,输出 "No"
return 0;
}

浙公网安备 33010602011771号