欧拉图,欧拉回路学习笔记

一笔画问题

在哥尼斯堡市区,普雷戈利亚河上有 \(7\) 座桥把河中心小岛和河岸连接起来。

当时有人提出了一个问题:一个行者怎样才能不重复、不遗漏地一次走完七座桥?

当时巨佬欧拉证明了这个问题,所以一笔画问题也称为欧拉路

欧拉路&欧拉回路

定义

欧拉路是经过图中每条边恰好一次的路径,欧拉回路是经过图中每条边恰好一次的回路。

我们大概理解一下,如果一个图存在一笔画,则一笔画的路径叫做欧拉路,如果最后又回到起点,那这个路径叫做欧拉回路

所以,欧拉回路也是欧拉路

判定方法

这里要分有向图无向图来讨论。

无向图

  • 图存在欧拉回路当且仅当满足度非 \(0\) 的节点互相可达没有奇数度数的节点
  • 图存在欧拉路当且仅当满足度非 \(0\) 的节点互相可达奇数度数的节点只有 \(0\) 个或者 \(2\)

可能会有疑惑,为什么是度非 \(0\) 的节点互相可达呢而不是全部都连通呢?

因为欧拉路是的集合,如果只有孤立的点,那么也是没有关系的。

说明一下,如果存在欧拉路,那么起点一定是在某一个奇数度数的点上,终点也是。

所以,如果没有奇数度数的点,起点和终点相连,就变成了欧拉回路

有向图

  • 图存在欧拉回路当且仅当满足度非 \(0\) 的节点是强连通的出度全部等于入度
  • 图存在欧拉路当且仅当满足有向边给成无向边后,度非 \(0\) 的节点互相可达只有两个节点入度和出度相差 \(1\)

说明一下,如果存在欧拉路,那么起点一定是在某一个奇数度数的点上,终点也是。

所以,如果没有奇数度数的点,起点和终点相连,就变成了欧拉回路

求欧拉路&欧拉回路的方法

求欧拉路&欧拉回路是用 DFS 来求的。

在回溯时去记录编号,最后加入起点,逆序输出。

为什么呢?如果存在欧拉路&欧拉回路,就一定是搜索出来的一条路径,回溯就能找到实际的路径,而搜索时记录会导致答案出现错误。

别忘了加入起点。

实现

有向图

为什么要分有向图和无向图呢?

我们想想为什么 DFS 图是 \(O(n+m)\) 的呢?

因为每个点会只访问 \(1\) 次。

但是欧拉路&欧拉回路会访问每个点多次,时间不就爆了吗?

怎么办呢?我们可以用一个小技巧,记录一个 \(f_x\),表示访问到了 \(x\) 节点的哪一个,这样子就不会重复枚举了,因为 \(f_x\) 是永远不回退的。

时间复杂度:\(O(n+m)\)

问一下,为什么不是 \(O(m)\) 的呢?

因为还要判定欧拉路&欧拉回路啊!

代码(欧拉回路改改判定就行了):

void dfs(int u) {
	while (f[u] < v[u]) {
		int v = adj[u][f[u]];
		++f[u];
		dfs(v);
		c[++l] = v;
	}
}

void Euler() {
	int s = 0, x = 0, y = 0; 
	for (int i = 1; i <= n; i++) {
		if (ind[i] + 1 == outd[i]) ++x, s = i;
		if (ind[i] != outd[i]) ++y;
	} 
	if (!(y == 0 || (x == 1 && y == 2))) {
		puts("No");
		return;
	}
	if (!s) for (int i = 1; i <= n; i++) {
		if (ind[i]) {
			s = i;
			break;
		}
	}
	memset(f, 0, sizeof(f));
	dfs(s);
	c[++l] = s;
	if (l != n + 1) {
		puts("No");
	} else {
		puts("Yes");
	}
	for (int i = l; i >= 1; i--) printf("%d", c[i]);
	puts("");
} 

无向图

其他和有向图一样,但是双向的该怎么办呢?

我们又来一个小技巧。我们发现 \(x \oplus 1\) 总是成对出现的,所以我们可以建一个编号来做这个事。

代码:

void dfs(int u) {
	while (f[u] < v[u]) {
		int v = adj[u][f[u]].first, idx = adj[u][f[u]].second;
		if (!b[idx]) {
			b[idx] = b[idx ^ 1] = true;
			++f[u];
			dfs(v);
			c[++l] = v;
		} else {
			++f[u];
		}
	}
}

void Euler() {
	int s = 0;
	for (int i = 1; i <= n; i++) {
		if (d[i] & 1) s = i;
	}
	if (!s) for (int i = 1; i <= n; i++) {
		if (d[i]) {
			s = i;
			break;
		}
	}
	l = 0;
	memset(b, false, sizeof(b));
	memset(f, 0, sizeof(f));
	dfs(s);
	c[++l] = s;
	for (int i = l; i >= 1; i--) printf("%d ", c[i]);
	puts("");
}

例题

P7771 【模板】欧拉路径

怎么让字典序更小呢?

因为在前面的肯定是先进入的,要想先进入的数更小,显然先排序。

#include <bits/stdc++.h>

using namespace std;
#define ll long long
#define ull unsigned long long
#define db double
#define all(x) x.begin(), x.end()
#define inf (1 << 30)
#define lnf (1LL << 60)
typedef pair<int, int> PII;
constexpr int N = 100000 + 7;
constexpr int P = 998244353;

int n, m; 
vector<int> adj[N];

int f[N], v[N], ind[N], outd[N], c[N], l;

void dfs(int u) {
	while (f[u] < v[u]) {
		int v = adj[u][f[u]];
		++f[u];
		dfs(v);
		c[++l] = v;
	}
}

void Euler() {
	int s = 0, x = 0, y = 0; 
	for (int i = 1; i <= n; i++) {
		if (ind[i] + 1 == outd[i]) ++x, s = i;
		if (ind[i] != outd[i]) ++y;
	} 
	if (!(y == 0 || (x == 1 && y == 2))) {
		puts("No");
		return;
	}
	if (!s) for (int i = 1; i <= n; i++) {
		if (ind[i]) {
			s = i;
			break;
		}
	}
	l = 0;
	memset(f, 0, sizeof(f));
	dfs(s);
	c[++l] = s;
	if (l != m + 1) {
		puts("No");
	} else {
		for (int i = l; i >= 1; i--) printf("%d ", c[i]);
	}
} 

int main() {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= m; i++) {
		int u, v;
		scanf("%d%d", &u, &v);
		++outd[u], ++ind[v];
		adj[u].push_back(v);
	}
	for (int i = 1; i <= n; i++) {
		v[i] = adj[i].size();
        sort(all(adj[i]));
	} 
	Euler();
	return 0;
}

其实,考到的次数也不多。

posted @ 2026-05-31 21:13  AKCoder  阅读(12)  评论(0)    收藏  举报