欧拉回路
一、相关定义
1. 欧拉通路
只通过一次图中的每条边,且经过图中所有顶点的通路为欧拉通路;
2. 欧拉回路
只通过一次图中的每条边,且经过图中所有顶点的回路为欧拉回路;
3. 有向图的基图
忽略有向边的方向,得到的无向图则为该有向图的基图;
4. 欧拉图
存在欧拉回路的图称为欧拉图;
5. 半欧拉图
存在欧拉通路的图称为半欧拉图;
二、判断与证明
1. 无向图
若无向图 G 为连通图,则可通过度的奇偶性判断图 G 是否存在欧拉通路或回路,有;
-
若图 G 不存在度为奇数的端点时,则图 G 有欧拉回路,即,无向连通多重图中存在欧拉回路当且仅当图中所有顶点的度数为偶数;
对于上述定理,证明如下;
-
先证明其充分性,即存在欧拉回路则图中的所有顶点的度数必然为偶数;
由于要遍历完图中所有的节点,则对于除起点外的每一个节点,一定在一次遍历时,通过一条边来到这个节点,并通过另一条边离开,所以其度数一定为偶数,则对于起点,通过一条边从起点出发,遍历所有节点,遍历完后,则再通过一条边返回,所以起点的度数也一定为偶数;
则得证;
-
再证明其必要性,即若连通图中所有顶点的度数为偶数,则必然存在欧拉回路;
使用构造性的存在性证明,则在所有顶点的度数为偶数的连通图中,选取一条回路,则
- 若此回路为欧拉回路,则结论成立了;
- 若此回路不为欧拉回路,则将则回路上的边删除,若出现孤点,则忽略,则删除边后的子图仍然保有原图的性质,即子图中间的节点的度数为偶数,且子图与删除掉的回路一定有公共顶点,以该点作为起点继续找回路,然后删除,重复以上过程,直到所有的边都被删除为止,则所有这些删除的回路一定可连接,构成了一条欧拉回路;
综上,得证;
综上,得证;
-
-
若图 G 存在且仅存在 2 个度为奇数的端点时,则图 G 有欧拉通路,其起点为其中 1 个度为奇数的端点,终点为另一个度为奇数的端点,即在无向连通多重图中存在欧拉通路且不存在欧拉回路当且仅当连通图中有且只用两个顶点的度数为奇数;
对于上述定理,证明如下,
-
先证明其充分性,即存在欧拉通路则图中有且只有两个顶点的度数为奇数,其他顶点的度数皆为偶数;
由于要遍历完图中所有的节点,则对于除起点与终点外的每一个节点,一定在一次遍历时,通过一条边来到这个节点,并通过另一条边离开,所以其度数一定为偶数,则对于起点与终点,通过一条边从起点出发,遍历所有节点,遍历完后,则再通过一条边到达终点,所以起点与终点的度数为奇数;
则得证;
-
再证明其必要性,即连通图中有且只有两个奇数度顶点,则必然存在欧拉通路;
则可将起点与终点进行连接,则原图中所有得节点度数均为偶数,又由于 连通图中所有顶点的度数为偶数,则必然存在欧拉回路 ,所以将连接的边删除后,可得到欧拉通路;
则得证;
综上,得证;
-
-
若不满足上述情况,则不存在欧拉回路与欧拉通路;
2. 有向图
若有向图 G 为连通图,则可通过出,入度的大小判断图 G 是否存在欧拉通路或回路,有;
-
若图 G 所有节点的入度等于出度,则图 G 有欧拉回路,即有向连通多重图中存在欧拉回路当且仅当图中所有顶点的入度数等于出度数;
证明如下,
由于要遍历完图中所有的节点,则对于除起点外的每一个节点,一定在一次遍历时,通过一条边来到这个节点,并通过另一条边离开,所以其入度等于出度,则对于起点,通过一条边从起点出发,遍历所有节点,遍历完后,则再通过一条边返回,所以起点的入度也一定等于出度;
则得证;
-
若图 G 存在且仅存在 2 个节点的入度不等于出度,且一个节点入度比出度大 1 ,一个入度比出度小 1 ,则图 G 有欧拉通路,其起点为入度比出度小 1 的节点,终点为节点入度比出度大 1 节点,即有向连通多重图中存在欧拉通路且不存在欧拉回路当且仅当连通图中有且只用两个顶点的入度不等于出度,且一个节点入度比出度大 1 ,另一个入度比出度小 1 ;
证明如下,
由于要遍历完图中所有的节点,则对于除起点与终点外的每一个节点,一定在一次遍历时,通过一条边来到这个节点,并通过另一条边离开,所以其入度等于出度,则对于起点,通过一条边从起点出发,遍历所有节点,不需返回,所以入度比出度小 1 ,对于终点,通过一条边到达,所以其入度比出度大 1 ;
则得证;
-
若不满足上述情况,则不存在欧拉回路与欧拉通路;
三、解法
1. DFS
思路
对于无向图,则寻找图中的度数为奇数的点,若没有则从任意节点开始搜索;
对于有向图,则寻找图中入度比出度小 1 的点,若没有则从任意节点开始搜索;
对节点 i i i 搜索时,搜索与 i i i 相邻的节点 u u u ,并删除 ( i , u ) (i, u) (i,u) 边,继续递归搜索 u u u 即可;
代码
以 欧拉回路 为例;
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
#define MAXN 200005
using namespace std;
int f, n, m, in[MAXN], out[MAXN], s, pos = 1, ans[MAXN], cnt;
bool vis[MAXN], vise[MAXN];
struct edge {
int to, tot;
};
vector <edge> g[MAXN];
void dfs(int i) {
vis[i] = true;
while (!g[i].empty()) { // 遍历并删除
int v = g[i].back().to, tot = g[i].back().tot;
g[i].pop_back();
if (!vise[abs(tot)]) {
vise[abs(tot)] = true; // 标记已走过的边
dfs(v);
ans[++cnt] = tot; // 存入路径
}
}
return;
}
int main() {
scanf("%d", &f);
scanf("%d %d", &n, &m);
for (int i = 1; i <= m; i++) {
int x, y;
scanf("%d %d", &x, &y);
g[x].push_back(edge({y, i}));
if (f == 1) g[y].push_back(edge({x, -i})); // 双向存边,记录反向
in[x]++, out[y]++;
}
if (m == 0) {
printf("YES\n");
return 0;
}
if (f == 1) {
for (int i = 1; i <= n; i++) {
if ((in[i] + out[i]) % 2 == 1) { // 找起点
printf("NO");
return 0;
} else if (in[i] + out[i]) {
pos = i;
}
}
} else {
for (int i = 1; i <= n; i++) {
if (in[i] != out[i]) { // 找起点
printf("NO");
return 0;
} else if (in[i]) {
pos = i;
}
}
}
dfs(pos); // 搜索
for (int i = 1; i <= n; i++) {
if ((in[i] || out[i]) && !vis[i]) { // 判断合法
printf("NO\n");
return 0;
}
}
printf("YES\n");
for (int i = cnt; i >= 1; i--) { // 输出
if (f == 2) ans[i] = abs(ans[i]);
printf("%d ", ans[i]);
}
return 0;
}
2 . Fleury
思路
设 G 为无向欧拉图,则求 G 中欧拉回路算法为,
- 取 G 中一顶点 v 1 v_1 v1 ,另 P 1 = v 1 P_1 = v_1 P1=v1 ;
- 假设沿
P
i
=
v
1
,
e
1
,
v
2
,
e
2
,
…
,
v
i
−
1
,
e
i
−
1
P_i = v_1, e_1, v_2, e_2, \dots ,v_{i - 1}, e_{i - 1}
Pi=v1,e1,v2,e2,…,vi−1,ei−1 走到点
v
i
v_i
vi ,则按下方法从
E
(
G
)
−
e
1
,
e
2
,
…
,
e
i
−
1
E(G) - {e_1, e_2, \dots ,e_{i - 1}}
E(G)−e1,e2,…,ei−1 中选
e
i
e_i
ei ;
- e i e_i ei 与 v i v_i vi 有连边;
- 除非无边选择,否则 e i e_i ei 不应是 E ( G ) − e 1 , e 2 , … , e i − 1 E(G) - {e_1, e_2, \dots ,e_{i - 1}} E(G)−e1,e2,…,ei−1 的桥;
- 2 无法继续时,算法停止;
结束时,得到的回路 P m = v 1 , e 1 , v 2 , e 2 , … , e m , v n P_m = v_1, e_1, v_2, e_2, \dots ,e_{m}, v_{n} Pm=v1,e1,v2,e2,…,em,vn 为欧拉回路;
代码
以 欧拉回路 为例;
#include <cstdio>
#include <stack>
#include <vector>
#include <cstring>
#include <algorithm>
#define MAXN 200005
using namespace std;
int f, n, m, in[MAXN], out[MAXN], pos = 1, ans[MAXN], cnt;
bool vis[MAXN], vise[MAXN];
struct edge {
int to, tot;
};
vector <edge> g[MAXN];
stack <int> s;
void dfs(int i) {
vis[i] = true;
while (!g[i].empty()) { // 遍历并删除
int v = g[i].back().to, tot = g[i].back().tot;
g[i].pop_back();
if (!vise[abs(tot)]) {
vise[abs(tot)] = true; // 标记已走过的边
dfs(v);
ans[++cnt] = tot; // 存入路径
}
}
return;
}
void fleury(int x) {
s.push(x);
while (!s.empty()) {
bool flag = false;
if (!g[s.top()].empty()) { // 有边相连
int y = s.top();
s.pop();
dfs(y);
} else { // 无边相连
s.pop();
}
}
return;
}
int main() {
scanf("%d", &f);
scanf("%d %d", &n, &m);
for (int i = 1; i <= m; i++) {
int x, y;
scanf("%d %d", &x, &y);
g[x].push_back(edge({y, i}));
if (f == 1) g[y].push_back(edge({x, -i})); // 双向存边,记录反向
in[x]++, out[y]++;
}
if (m == 0) {
printf("YES\n");
return 0;
}
if (f == 1) {
for (int i = 1; i <= n; i++) {
if ((in[i] + out[i]) % 2 == 1) { // 找起点
printf("NO");
return 0;
} else if (in[i] + out[i]) {
pos = i;
}
}
} else {
for (int i = 1; i <= n; i++) {
if (in[i] != out[i]) { // 找起点
printf("NO");
return 0;
} else if (in[i]) {
pos = i;
}
}
}
fleury(pos); // 搜索
for (int i = 1; i <= n; i++) {
if ((in[i] || out[i]) && !vis[i]) { // 判断合法
printf("NO\n");
return 0;
}
}
printf("YES\n");
for (int i = cnt; i >= 1; i--) { // 输出
if (f == 2) ans[i] = abs(ans[i]);
printf("%d ", ans[i]);
}
return 0;
}