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;
}