You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

11 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

AcWing 1184. 欧拉回路

一、题目描述

给定一张图,请你 找出欧拉回路,即在图中找一个环使得每条边都在环上出现恰好一次。

输入格式 第一行包含一个整数 tt∈{1,2},如果 t=1,表示所给图为无向图,如果 t=2,表示所给图为有向图。

第二行包含两个整数 n,m,表示图的结点数和边数。

接下来 m 行中,第 i 行两个整数 v_i,u_i,表示第 i 条边(从 1 开始编号)。

如果 t=1 则表示 v_iu_i 有一条无向边。 如果 t=2 则表示 v_iu_i 有一条有向边。

图中可能有重边也可能有自环

点的编号从 1n

输出格式 如果无法一笔画出欧拉回路,则输出一行: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

二、解题思路

定义

欧拉路径:通过图中每条边恰好一次的路径 欧拉回路:通过图中每条边恰好一次的回路

无向图(边连通)

  • 若起点与终点不同:则起点与终点度数为奇数,其它点度数为偶数
    • 存在欧拉路径的充要条件:存在02个点的度数为奇数
  • 若起点与终点相同:则不存在某点的度数为奇数
    • 存在欧拉回路的充要条件:不存在度数为奇数的点

对有向图(边连通)

  • 若起点与终点不同:则起点的出度比入度多1,终点入度比出度多1,其它点的出度与入度相同
    • 存在欧拉路径的充要条件:所有点出度与入度相同,或者仅存在一个出度比入度多1的点(起点)和一个点入度比出度多1的点(终点),其它点出度与入度相同
  • 若起点与终点相同:则所有点的出度与入度相同
    • 存在欧拉回路的充分必要条件:所有点的出度与入度相同

求欧拉路径/欧拉回路(需保证边连通)

利用dfs求欧拉路径/回路:在遍历完当前节点的所有邻接点后,将该点加入到序列中,在做完dfs后,欧拉回路为序列的逆序。

dfs求欧拉路径模拟

如上图所示,图中含多个顶点,其中只标出ABC方便算法描述。 在进行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条自环边的编号由内向外递增012… 第一次错误的修改和最终正确的修改的区别在于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;
}