【ACWing】1184. 欧拉回路

题目地址:

https://www.acwing.com/problem/content/1186/

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

输入格式:
第一行包含一个整数 t t t t ∈ { 1 , 2 } t∈\{1,2\} t{ 1,2},如果 t = 1 t=1 t=1,表示所给图为无向图,如果 t = 2 t=2 t=2,表示所给图为有向图。第二行包含两个整数 n , m n,m n,m,表示图的结点数和边数。接下来 m m m行中,第 i i i行两个整数 v i , u i v_i,u_i vi,ui,表示第 i i i条边(从 1 1 1开始编号)。如果 t = 1 t=1 t=1则表示 v i v_i vi u i u_i ui有一条无向边。如果 t = 2 t=2 t=2则表示 v i v_i vi u i u_i ui有一条有向边。图中可能有重边也可能有自环。点的编号从 1 1 1 n n n

输出格式:
如果无法一笔画出欧拉回路,则输出一行:NO。否则,输出一行:YES,接下来一行输出任意一组合法方案即可。如果 t = 1 t=1 t=1,输出 m m m个整数 p 1 , p 2 , … , p m p_1,p_2,…,p_m p1,p2,,pm。令 e = ∣ p i ∣ e=|p_i| e=pi,那么 e e e表示经过的第 i i i条边的编号。如果 p i p_i pi为正数表示从 v e v_e ve走到 u e u_e ue,否则表示从 u e u_e ue走到 v e v_e ve。如果 t = 2 t=2 t=2,输出 m m m个整数 p 1 , p 2 , … , p m p_1,p_2,…,p_m p1,p2,,pm。其中 p i p_i pi表示经过的第 i i i条边的编号。

数据范围:
1 ≤ n ≤ 1 0 5 1≤n≤10^5 1n105
0 ≤ m ≤ 2 × 1 0 5 0≤m≤2×10^5 0m2×105

一个无向图含欧拉回路,当且仅当其边连通并且每个顶点的度是偶数。证明如下:
必要性:如果存在欧拉回路,则取该回路,其经过的所有顶点必然是有入边必有出边(除了出发点,对于出发点是有出必有入),所以该路径上的所有点的度是偶数。而孤立点的度是 0 0 0,也是偶数,所以得证。
充分性:如果每个顶点的度都是偶数,那么从任一点出发,先走出一个回路,这样的回路一定是能走出来的,因为每次到一个顶点的时候,有入必有出,除非走回出发点,否则就能一直走下去,直到走到出发点为止。如果该回路已经包含了所有边了,那就找到了一条欧拉回路了;否则,从原图中删去该欧拉回路以及所有的孤立顶点,所得子图依然是所有点都是度为偶数的子图,并且它与欧拉回路一定存在公共顶点 u u u,从 u u u再重复上述过程(这里也可以类似数学归纳法来做,按归纳假设,求出从 u u u出发的欧拉回路),然后将这个回路拼到原回路上去即可。

而对于一个有向图是否含欧拉回路,也有个充要条件,即其边连通,并且每个点的入度等于出度。证明类似无向图的情形。

无论其是有向图还是无向图,算法都是一样的。

首先考虑建图,我们依然是用链式前向星(即邻接表)来建图,对于无向图,由于某条边有可能是走正向,也有可能是走负向,这无法确定,所以我们要建立双向边,但是标记访问过的时候,需要在正向走过的时候同时再标记一下负向也走过了(反之亦然)。由于每次建立一条边的时候,我们都会同时建立其反向边,正向边在数组中的下标是偶数,反向边在数组中的下标是奇数(即一条正向反向边的下标分别是 ( 0 , 1 ) , ( 2 , 3 ) , . . . (0,1),(2,3),... (0,1),(2,3),...这样,正向边都是偶数,反向边都是奇数),有必要梳理出某条边在数组中的编号、该边本身的编号以及其反向边的编号之间的关系。如果在输入里,某条边的编号是 k k k(从 0 0 0计数。注意在题目里是从 1 1 1计数的,所以最后还要加上 1 1 1),那么其本身在数组中的编号就是 2 k 2k 2k,而其反向边的编号是 2 k + 1 2k+1 2k+1;而如果知道了一条边在数组中的下标,那么其对应的反向边的编号取决于其自己是奇数还是偶数,如果是偶数则减 1 1 1,否则是加 1 1 1,在代码里可以用异或来做,具体来说就是 2 k ∧ 1 = 2 k + 1 , ( 2 k + 1 ) ∧ 1 = 2 k 2k\wedge 1=2k+1,(2k+1)\wedge 1=2k 2k1=2k+1,(2k+1)1=2k

其次考虑算法,这可以用DFS后序遍历来做。之所以不用前序遍历,是因为在搜索的顺序的边与边之间不一定是邻接的,搜索出来的路径也不是一条连通的路径。而后序遍历则不然,如果确实存在欧拉路径,在分叉处的一条路回溯到分叉点的时候,如果该分叉处的所有树枝都被遍历过了,那么回溯的路径就是一条完整连续的路径,其本身就是一条欧拉路;如果还有树枝没遍历,则会按照该路继续DFS,而这次DFS一定还会回到分叉点,所以回溯的时候刚好会形成一个连通的路径,并且会把所有边都含到路径里去。即后序遍历,既不遗漏,又产生了连通的路径,完美解决了问题。

对于本题,可以先去找一个非孤立点,如果找不到的话则看边数是否是 0 0 0,如果是 0 0 0那确实是欧拉图(说明该图的所有点都是孤立点),否则的话,就从其中任意一个非孤立点开始做DFS,遍历过的边不再遍历,同时在回溯之前将当前边加入答案。这里有一个细节可以优化,如果我们只是对遍历过的边,用个bool数组进行标记的话,那么事实上仍然会遍历这一串指针,这也是会消耗很多时间的。所以,我们可以将遍历过的边直接删掉。但是由于我们的存储方式,对于正在遍历的那条边的反向边而言,即使知道它在数组中的下标,仍然不方便删,原因是我们是按照单链表的方式存的,要删就要从起点一步步向后找。所以我们采取的方案是,对于顶点 v v v,从 h [ v ] h[v] h[v]开始遍历其所有邻边的时候,如果是有向图,那非常简单,遍历完这条边立刻删掉即可;而对于无向图,则要先用一个bool数组标记其反向边为访问过,下次遍历这条反向边的时候由于已经标记过,那时再将其删去。

代码如下:

#include <iostream>
#include <cstring>
using namespace std;

// 要建反向边,所以边数要开两倍
const int N = 100100, M = 400100;
int h[N], e[M], ne[M], idx;
int res[M], cnt;
// 记录某条边是否被访问过(主要是应付无向图的反向边)
bool used[M];
// din[i]和dout[i]分别记录顶点i的入度和出度
int din[N], dout[N];
int n, m;
// 记录图的类型,1表示无向图,2表示有向图
int type;

void add(int a, int b) {
    
    
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

// 从顶点u开始DFS
void dfs(int u) {
    
    
	// 如果顶点u还有边可以遍历,则进入循环
    while (h[u] != -1) {
    
    
        // 如果这条边用过了,删除这条边。这条边事实上就是之前遍历的某条边的反向边
        if (used[h[u]]) {
    
    
            h[u] = ne[h[u]];
            continue;
        }

        // 标记这条边已使用。其实这句话可以省去不写,后面的代码可以看出这个循环结束后这条边就已经被删了
        // used[h[u]] = true;
        
        // 如果是无向图,那么这条边的反向边也要标记使用过了
        if (type == 1) used[h[u] ^ 1] = true;

		// 求一下当前遍历的这条边的编号
        int t;
        if (type == 1) {
    
    
        	// 题目里边是从1开始计数的,所以后面要 + 1
            t = h[u] / 2 + 1;
            // (0, 1) (2, 3) (4, 5)奇数编号是返回的边
            if (h[u] & 1) t = -t;
        } else t = h[u] + 1;
		
		// 删掉当前的边,并继续DFS
        int j = e[h[u]];
        h[u] = ne[h[u]];
        dfs(j);
        // 回溯之前存一下路径
        res[cnt++] = t;
    }
}

int main() {
    
    
    scanf("%d%d%d", &type, &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) {
    
    
                //无向图含欧拉回路的充要条件是每个点的度都为偶数
                cout << "NO" << endl;
                return 0;
            }
    } else {
    
    
        for (int i = 1; i <= n; i++)
            if (din[i] != dout[i]) {
    
    
                //有向图含欧拉回路的充要条件是每个点的入度等于出度
                cout << "NO" << endl;
                return 0;
            }
    }

	// 找到一个有邻边的点开始DFS
    for (int i = 1; i <= n; i++) 
        if (~h[i]) {
    
    
            dfs(i);
            break;
        }

	// 如果一次DFS未遍历完所有的边,说明不边连通,不存在欧拉回路,输出NO
    if (cnt < m) cout << "NO" << endl;
    else {
    
    
        cout << "YES" << endl;
        // 逆序输出DFS回溯的路径
        for (int i = cnt - 1; i >= 0; i--)
            cout << res[i] << " ";
    }

    return 0;
}

时间复杂度 O ( m ) O(m) O(m),空间 O ( n + m ) O(n+m) O(n+m)

猜你喜欢

转载自blog.csdn.net/qq_46105170/article/details/115060171