AcWing 1184. 欧拉回路
\(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] = 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];
if (st[i]) continue;
// 标识i这条边已使用过
st[i] = 1;
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;
}