11 KiB
AcWing 1184. 欧拉回路
一、题目描述
给定一张图,请你 找出欧拉回路,即在图中找一个环使得每条边都在环上出现恰好一次。
输入格式 第一行包含一个整数 $t$,$t∈{1,2}$,如果 $t=1$,表示所给图为无向图,如果 $t=2$,表示所给图为有向图。
第二行包含两个整数 $n,m$,表示图的结点数和边数。
接下来 m 行中,第 i 行两个整数 $v_i,u_i$,表示第 i 条边(从 1 开始编号)。
如果 t=1 则表示 v_i 到 u_i 有一条无向边。
如果 t=2 则表示 v_i 到 u_i 有一条有向边。
图中可能有重边也可能有自环。
点的编号从 1 到 $n$。
输出格式 如果无法一笔画出欧拉回路,则输出一行:$NO$。
否则,输出一行:$YES$,接下来一行输出 任意一组 合法方案即可。
如果 $t=1$,输出 m 个整数 $p_1,p_2,…,p_m$。令 $e=|p_i|$,那么 e 表示经过的第 i 条边的编号。如果 p_i 为正数表示从 v_e 走到 $u_e$,否则表示从 u_e 走到 $v_e$。
如果 $t=2$,输出 m 个整数 $p_1,p_2,…,p_m$。其中 p_i 表示经过的第 i 条边的编号。
数据范围
1≤n≤10^5,
0≤m≤2×10^5
输入样例1:
1
3 3
1 2
2 3
1 3
输出样例1:
YES
1 2 -3
输入样例2:
2
5 6
2 3
2 5
3 4
1 2
4 2
5 1
输出样例2:
YES
4 1 3 5 2 6
二、解题思路
定义
欧拉路径:通过图中每条边恰好一次的路径 欧拉回路:通过图中每条边恰好一次的回路
无向图(边连通)
- 若起点与终点不同:则起点与终点度数为奇数,其它点度数为偶数
- 存在欧拉路径的充要条件:存在$0$或$2$个点的度数为奇数
- 若起点与终点相同:则不存在某点的度数为奇数
- 存在欧拉回路的充要条件:不存在度数为奇数的点
对有向图(边连通)
- 若起点与终点不同:则起点的出度比入度多$1$,终点入度比出度多$1$,其它点的出度与入度相同
- 存在欧拉路径的充要条件:所有点出度与入度相同,或者仅存在一个出度比入度多$1$的点(起点)和一个点入度比出度多$1$的点(终点),其它点出度与入度相同
- 若起点与终点相同:则所有点的出度与入度相同
- 存在欧拉回路的充分必要条件:所有点的出度与入度相同
求欧拉路径/欧拉回路(需保证边连通)
利用$dfs$求欧拉路径/回路:在遍历完当前节点的所有邻接点后,将该点加入到序列中,在做完$dfs$后,欧拉回路为序列的逆序。

$dfs$求欧拉路径模拟
如上图所示,图中含多个顶点,其中只标出$A,B,C$方便算法描述。 在进行$dfs$遍历时: 路径$1$:从$A$点出发,遍历到$B$; 路径$2$:从$B$点出发,遍历到$C$; 路径$4$:回溯到$B$,依次添加节点到序列中,添加的节点顺序即为路径$4$; 路径$3$:从$B$点出发,访问第$3$条边,直到遍历到自身; 路径$5$:回溯到$B$,依次添加节点到序列中,添加的节点顺序即为路径$5$; 路径$6$:回溯到$A$,依次添加节点到序列中,添加的节点顺序即为路径$6$;
- ① 我们在遍历每个节点时将其添加到序列中,得到的序列为路径$1$,路径$2$,路径$3$,显然不是欧拉路径;
- ② 我们在遍历完每个节点的所有邻接点后将其添加进序列,得到的序列为路径$4$,路径$5$,路径$6$,将序列逆置之后,得到路径$1$,路径$3$,路径$2$,即欧拉路径。
$dfs$变形
在普通的$dfs$中,我们通常选择对点进行判重,但在求欧拉路径的时候,因为存在环,所以一个点可能被遍历多次,因此我们采取 对边判重。
$dfs$删边优化
因为欧拉路径中每条边只走一次,因此我们可以在每条边被遍历之后把它删除,以节省判重时间,达到时间上的优化。
$yxc$视频课中第一次错误优化
void dfs(int u){
for(int i = h[u] ; ~i ; i = ne[i]){
if(st[i]){
h[u] = ne[i];
continue;
}
st[i] = 1;
if(type == 1)st[i ^ 1] = 1;
int t;
if(type == 1){
t = i / 2 + 1;
if(i & 1)t = -t;
}
else t = i + 1;
h[u] = ne[i];
dfs(e[i]);
res[++ cnt] = t;
}
}
正确优化
void dfs(int u){
for(int i = h[u] ; ~i ; i = h[u]){//区别
if(st[i]){
h[u] = ne[i];
continue;
}
st[i] = 1;
if(type == 1)st[i ^ 1] = 1;
int t;
if(type == 1){
t = i / 2 + 1;
if(i & 1)t = -t;
}
else t = i + 1;
h[u] = ne[i];
dfs(e[i]);
res[++ cnt] = t;
}
}
代码解释
考虑第一个点有$n$条自环的情况,并且假设$n$条自环边的编号由内向外递增$0,1,2$… 第一次错误的修改和最终正确的修改的区别在于$for$循环中$i$是如何改变的
1.错误的i = ne[i]2.正确的i = h[u]
Q:为什么$1$是错的呢?
因为即使在循环内已经把$h[u]$改为了$ne[i]$,但在第一层$dfs$中,$i$能被$h[u]$影响到的只有在第一次赋值时,所以即使把h[u] = ne[i],但在回溯到第一层时,i = ne[i],而$ne[i]$即为第一条边的下一条边(也就是第二条边,如此往复,第二条边的下一边是第三条边…即还是要循环$n$次)。同理,在第二层时,因为在第一层已经把$h[u]$改为了第一层的$ne[i]$即第二条边,所以第二层从第二条自环开始循环,要循环$n-1$次,总的时间复杂度还是平方级别
Q:为什么$2$是对的呢?
因为在$i$改变由$h[u]$决定,在第一层时$h[u]$已经变为第二条边,所以到第二层时从第二条边开始循环,在第二层时$h[u]$又改为了第三条边,所以第三层从第三条边开始....以上和$1$是一样的,区别在于回溯的时候,比如回溯到了第一层,$i$是通过$h[u]$来改变的,而此时$h[u]$已经变成了$-1$所以跳出循环,第一层只执行了一次,所以是线性的。
思考方式
用多个自环的用例,手动模拟一下,就明白这个道理了:

五、实现代码
#include <bits/stdc++.h>
using namespace std;
const int N = 100010, M = 400010;
int T; // 1:无向图,2:有向图
int n, m; // n个节点,m条边
int din[N], dout[N]; // 入度,出度,如果是无向图,则din[u]+dout[u]=u点的度
int cnt, ans[M]; // 欧拉回路路径
// 链式前向星
int e[M], h[N], idx, w[M], ne[M];
void add(int a, int b, int c = 0) {
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
int st[M]; // 某条边是否已访问过
// 1、无向图
void dfs1(int u) {
for (int i = h[u]; ~i; i = h[u]) {
h[u] = ne[i];
if (st[i]) continue;//这个st[i]是给成对变换的无向图专用的,因为一边走一边删边,有向图的边都删除掉了,还记录啥走没走过,只有无向图才有这个,因为成对变换的边还没有删除掉,需要标识一下
//st[i] = 1, st[i ^ 1] = 1; // 无向图,成对变换的边也标识为已使用过
st[i ^ 1] = 1; // 无向图,成对变换的边也标识为已使用过
// 本题的特殊要求
int t = i / 2 + 1; // 无向图计算边号,注意i从0开始,所以需要除2再加1
if (i & 1) t = -t; // 偶数边u->v,奇数边v->u,题目要求v->u输出一个负号
// 注意是回溯时再记录,题解中有论述原因
dfs1(e[i]);
// 记录路径
ans[++cnt] = t;
}
}
// 2、有向图
void dfs2(int u) {
for (int i = h[u]; ~i; i = h[u]) {
h[u] = ne[i];
int t = i + 1; // 有向图计算边号,注意i从0开始,所以加1
// 注意是回溯时再记录,题解中有论述原因
dfs2(e[i]);
// 记录路径
ans[++cnt] = t;
}
}
int main() {
scanf("%d%d%d", &T, &n, &m); // 图的类型
memset(h, -1, sizeof h); // 链式前向星
for (int i = 0; i < m; i++) { // 后面的m条数是有用的,如果路径中最终的数量小于m,表示未能找到欧拉回路,不能用while(m--)替换
int a, b;
scanf("%d%d", &a, &b);
add(a, b);
if (T == 1) add(b, a);
din[b]++, dout[a]++; // 有向图:记录入度和出度;无向图:D(u)指经过u的无向边数量=din[u]+dout[u]
}
if (T == 1) {
for (int i = 1; i <= n; i++)
if ((din[i] + dout[i]) & 1) { // 无向图中,如果某个点的度是奇数,则不存在欧拉回路
puts("NO");
return 0;
}
} else {
for (int i = 1; i <= n; i++)
if (din[i] != dout[i]) { // 有向图中,如果某个点的入度与出度不等,则不存在欧拉回路
puts("NO");
return 0;
}
}
// 开始找欧拉回路
for (int u = 1; u <= n; u++)
// 举例:如果是两个欧拉回路图,那么两者之间彼此不连通,此时也不欧拉回路,我们找到一个非独立点开始出发,找出所有的边,
// 再通过检查边的数量与真实边的数量对比,才能真正知道是不是存在欧拉回路
if (~h[u]) {
if (T == 1)
dfs1(u);
else if (T == 2)
dfs2(u);
break;
}
// 如果找到的欧拉回路路径条数小于总边数m,表示没有找出全部边,说明无欧拉回路
if (cnt < m) {
puts("NO");
return 0;
}
// 存在欧拉回路
puts("YES");
// 逆序输出序列
for (int i = cnt; i; i--) printf("%d ", ans[i]);
return 0;
}

