|
|
|
|
## [$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
|
|
|
|
|
```
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
### 二、解题思路
|
|
|
|
|
|
|
|
|
|
#### 定义
|
|
|
|
|
欧拉路径:通过图中每条边恰好一次的路径
|
|
|
|
|
欧拉回路:通过图中每条边恰好一次的回路
|
|
|
|
|
|
|
|
|
|
#### 无向图(边连通)
|
|
|
|
|
- 若起点与终点不同:则起点与终点度数为奇数,其它点度数为偶数
|
|
|
|
|
- 存在欧拉路径的充要条件:存在$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$视频课中第一次错误优化
|
|
|
|
|
```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$所以跳出循环,第一层只执行了一次,所以是线性的。
|
|
|
|
|
|
|
|
|
|
**思考方式**
|
|
|
|
|
用多个自环的用例,手动模拟一下,就明白这个道理了:
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 五、实现代码
|
|
|
|
|
```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;
|
|
|
|
|
}
|
|
|
|
|
```
|