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

11 KiB
Raw Blame History

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