欧拉回路/路径

先讲一下定义:

  • 欧拉路径是一个图上从一个点到另一个点的路径,使得每一条边都被走过了。

  • 欧拉回路是一个图上从一个点回到自己的环,使得每一条边都被走过了。

我们不妨先看一下这道题: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;
}
posted @ 2026-08-27 15:09  wusixuan  阅读(16)  评论(0)    收藏  举报