1. 问题描述:
给定一张图,请你找出欧拉回路,即在图中找一个环使得每条边都在环上出现恰好一次。
输入格式
第一行包含一个整数 t,t∈{1,2},如果 t=1,表示所给图为无向图,如果 t=2,表示所给图为有向图。第二行包含两个整数 n,m,表示图的结点数和边数。接下来 m 行中,第 i 行两个整数 vi,ui,表示第 i 条边(从 1 开始编号)。
如果 t=1 则表示 vi 到 ui 有一条无向边。
如果 t=2 则表示 vi 到 ui 有一条有向边。
图中可能有重边也可能有自环。点的编号从 1 到 n。
输出格式
如果无法一笔画出欧拉回路,则输出一行:NO。否则,输出一行:YES,接下来一行输出 任意一组 合法方案即可。
如果 t=1,输出 m 个整数 p1,p2,…,pm。令 e=|pi|,那么 e 表示经过的第 i 条边的编号。如果 pi 为正数表示从 ve 走到 ue,否则表示从 ue 走到 ve。
如果 t=2,输出 m 个整数 p1,p2,…,pm。其中 pi 表示经过的第 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
来源:https://www.acwing.com/problem/content/description/1186/
2. 思路分析:
分析题目可以知道这道题目属于欧拉回路的裸题,分为两种情况:
- 对于无向图,所有节点的度数必须是偶数,并且所有的边必须连通
- 对于有向图,所有节点的入度等于出度,并且所有的边必须连通
因为存在重边和自环,而且使用的是python语言所以不能够使用[list() for i in range(n + 10)]来建图,因为最终输出欧拉回路的时候需要输出在一开始输入的对应的边的编号,这里可以使用邻接表的方式来建图,其中需要使用到三个数组,分别是h,e和ne,h表示图中节点的表头,e存储边的终点,ne存储边的邻节点(存储下一条边的编号),使用这种方式建图的有三个好处:第一个好处:因为这种方式在建图的时候每一条边都存在唯一的一个编号,所以对于存在重边也不会有影响,而对于python语言使用列表的方式来建图需要使用额外的数据结构来标记当前的边是第几条边的信息,可以使用列表嵌套字典,字典再嵌套一个字典进行标记,但是这样标记很麻烦);第二个好处是在搜索的时候很容易标记反向边,这种方式创建无向图的时候对应的边的编号:0-1为一组双向边,2-3为一组双向边,...所以对于当前遍历的边i,其反向边为i ^ 1,对于无向边我们在创建的时候需要创建两个方向,但是在搜索的时候只搜索一次即可,所以当为无向图的时候搜索完当前的边之后标记对应的反向边也搜索过了;第三个好处:题目中有一个比较蛋疼的地方是对于输入从vi到ui的无向边,但是在输出欧拉回路的时候如果从ui到vi,那么要标记为反向边,i表示当前搜索的是第i条边,如果发现i是奇数说明是反向边,所以需要标记为对应的负数即可。欧拉路径与欧拉回路都基于深度优先遍历,其实在dfs的时候标记欧拉路径与欧拉回路也比较简单,只需要遍历完当前节点u的所有邻接点之后那么将当前的边的信息加入到欧拉路径或者欧拉回路中即可,这样最终得到的是欧拉路径或者欧拉回路的逆序。因为这道题目存在无解的情况,所以我们需要声明两个数组din,dout来记录入度和出度,我们可以将无向边看成是特殊的有向边,当所有的边输入完成之后根据输入的是无向图还是有向图判断是否存在无解即可。这道题目有一些比较坑的数据,可能存在很多个自环的情况,这个时候需要优化一下,当我们遍历完当前边之后,修改当前边的表头为下一条边的编号,这样递归回溯遍历邻接点的往下递归的时候由于表头修改过了所以不会搜索之前搜索过的边,如果没有修改表头那么递归回溯遍历下一个邻节点的时候还会继续遍历之前的搜过的边。
3. 代码如下:
c++代码:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010, M = 400010;
int type;
int n, m;
int h[N], e[M], ne[M], idx;
bool used[M];
int ans[M], cnt;
int din[N], dout[N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void dfs(int u)
{
for (int i = h[u]; ~i;i = h[u])
{
if (used[i]){
h[u] = ne[i];
continue;
}
used[i] = true;
if (type == 1) used[i ^ 1] = true;
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]);
ans[ ++ cnt] = t;
}
}
int main()
{
scanf("%d", &type);
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
for (int i = 0; i < m; i ++ )
{
int a, b;
scanf("%d%d", &a, &b);
add(a, b);
if (type == 1) add(b, a);
din[b] ++ , dout[a] ++ ;
}
if (type == 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 i = 1; i <= n; i ++ )
if (h[i] != -1)
{
dfs(i);
break;
}
if (cnt < m)
{
puts("NO");
return 0;
}
puts("YES");
for (int i = cnt; i; i -- ) printf("%d ", ans[i]);
puts("");
return 0;
}
python代码:递归爆栈,只过了28个数据,其实python使用dfs对于数据量大的时候是非常不友好的要么就超时,要么就爆栈:
import sys
class Solution:
type = 0
h, ne, e, idx, used, res = None, None, None, None, None, None
# 加边函数
def add(self, a: int, b: int):
self.e[self.idx] = b
self.ne[self.idx] = self.h[a]
self.h[a] = self.idx
self.idx += 1
# 这里存在很多个自环的情况所以在递归python绝对会爆栈
def dfs(self, u: int):
i = self.h[u]
while i != -1:
# i表示当前是第i条的边, 如果当前第i条边没有用过那么当前节点u的表头跳到u的邻接点上, 也即下一条边
if self.used[i] == 1:
# 优化: 修改h[u]可以使得回溯在往下递归的时候避免很多个自环又遍历重复的边的情况
self.h[u] = self.ne[i]
i = self.h[u]
# 跳过当前的边
continue
# 标记当前的边已经被访问
self.used[i] = 1
if self.type == 1:
# 因为是无向图所以要标记反向边, 这里使用h, e, ne三个数组来建图有一个好处是非常容易找到反向边, i ^ 1就是i的反向边
self.used[i ^ 1] = 1
t = 0
# 当无向边的时候0-1是一组边, 2-3是一组边, 4-5是一组边...所以边的编号应该是i // 2 + 1
if self.type == 1:
t = i // 2 + 1
# 由题目要求标记反向边, 可以发现i是奇数的时候那么说明存在反向边
if i & 1: t = -t
else:
# 因为边数是从0开始的, 所以当为有向图的时候i + 1就是当前的第几条边
t = i + 1
# j为当前边的下一个终点
j = self.e[i]
# 优化: 修改表头跳到下一条边
self.h[u] = self.ne[i]
self.dfs(j)
# 递归完当前节点的邻接点之后
self.res.append(t)
i = self.h[u]
def process(self):
self.type = int(input())
n, m = map(int, input().split())
self.h, self.e, self.ne, self.used = [-1] * (n + 10), [0] * (m + 10) * 2, [0] * (m + 10) * 2, [0] * (m + 10) * 2
self.idx = 0
# 统计一个节点的入度和出度, 这样可以判断是否无解
din, dout = [0] * (n + 10), [0] * (n + 10)
for i in range(m):
a, b = map(int, input().split())
self.add(a, b)
# 这里将无向边看成是特殊的有向边
din[b] += 1
dout[a] += 1
if self.type == 1:
# 添加反向边
self.add(b, a)
if self.type == 1:
for i in range(1, n + 1):
# 存在奇数说明节点i出去和进来的边的数量不相等那么不存在欧拉回路
if din[i] + dout[i] & 1:
print("NO")
return
else:
# 有向图那么每一个节点的入度 = 出度
for i in range(1, n + 1):
if din[i] != dout[i]:
print("NO")
return
self.res = list()
for i in range(1, n + 1):
# 表头不空的时候那么往下递归
if self.h[i] != -1:
self.dfs(i)
break
if len(self.res) < m:
print("NO")
return
print("YES")
for i in range(len(self.res) - 1, -1, -1):
print(self.res[i], end=" ")
if __name__ == "__main__":
sys.setrecursionlimit(100000)
Solution().process()