AcWing 1184. 欧拉回路

\(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] = 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];
        if (st[i]) continue;

        // 标识i这条边已使用过
        st[i] = 1;
        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;
}
posted @ 2022-04-06 15:48  糖豆爸爸  阅读(127)  评论(1编辑  收藏  举报
Live2D