Files
python/TangDou/AcWing/EulerPath/1184.md
HuangHai 1f397eca87 'commit'
2025-08-30 18:35:01 +08:00

286 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## [$AcWing$ $1184$. 欧拉回路](https://www.acwing.com/problem/content/1186/)
### 一、题目描述
给定一张图,请你 **找出欧拉回路**,即在图中找一个环使得每条边都在环上出现恰好一次。
**输入格式**
第一行包含一个整数 $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**:
```cpp {.line-numbers}
1
3 3
1 2
2 3
1 3
```
**输出样例1**:
```cpp {.line-numbers}
YES
1 2 -3
```
**输入样例2**:
```cpp {.line-numbers}
2
5 6
2 3
2 5
3 4
1 2
4 2
5 1
```
**输出样例2**:
```cpp {.line-numbers}
YES
4 1 3 5 2 6
```
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20230803093454.png)
### 二、解题思路
#### 定义
欧拉路径:通过图中每条边恰好一次的路径
欧拉回路:通过图中每条边恰好一次的回路
#### 无向图(边连通)
- 若起点与终点不同:则起点与终点度数为奇数,其它点度数为偶数
- 存在欧拉路径的充要条件:存在$0$或$2$个点的度数为奇数
- 若起点与终点相同:则不存在某点的度数为奇数
- 存在欧拉回路的充要条件:不存在度数为奇数的点
#### 对有向图(边连通)
- 若起点与终点不同:则起点的出度比入度多$1$,终点入度比出度多$1$,其它点的出度与入度相同
- 存在欧拉路径的充要条件:所有点出度与入度相同,或者仅存在一个出度比入度多$1$的点(起点)和一个点入度比出度多$1$的点(终点),其它点出度与入度相同
- 若起点与终点相同:则所有点的出度与入度相同
- 存在欧拉回路的充分必要条件:所有点的出度与入度相同
#### 求欧拉路径/欧拉回路(需保证边连通)
利用$dfs$求欧拉路径/回路:在遍历完当前节点的所有邻接点后,将该点加入到序列中,在做完$dfs$后,欧拉回路为序列的逆序。
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20230803100203.png)
#### $dfs$求欧拉路径模拟
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20230803082433.png)
如上图所示,图中含多个顶点,其中只标出$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$视频课中第一次错误优化
```cpp {.line-numbers}
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;
}
}
```
**正确优化**
```cpp {.line-numbers}
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$所以跳出循环,第一层只执行了一次,所以是线性的。
**思考方式**
用多个自环的用例,手动模拟一下,就明白这个道理了:
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20230803101825.png)
### 五、实现代码
```cpp {.line-numbers}
#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;
}
```