欧拉回路和欧拉路径

欧拉回路和欧拉路径

1. 欧拉回路和欧拉路径原理

原理

  • 欧拉路径:从一个点出发,到某个点停止,且中间每条边值只经过一次的路径。(点可以经过多次)

  • 对于无向图,所有边都是连通的。

    (1)存在欧拉欧拉路径的充分必要条件:度数为奇数的点只能有0个或2个;

    (2)存在欧拉欧拉回路的充分必要条件:度数为奇数的点只能有0个;

    必要性很容易得出,充分性可以构造性证明,这里省略。

  • 对于有向图,所有边都是连通的。

    (1)存在欧拉欧拉路径的充分必要条件:要么所有点的出度等于入度;要么除了两个点之外,其余所有点的出度等于入度,剩余的两个点:一个满足出度比入度多1(起点),另一个满足入度比出度多1(终点);

    (2)存在欧拉欧拉回路的充分必要条件:所有点的出度等于入度;

  • 伪码描述

void dfs(int u) {
    for 从u出发的所有边i:
    	dfs(i);
    ans <- u;
}
  • 最终ans中存储的是欧拉路径的逆序。这里值得一提的是:当我们按照从小到大的顺序遍历所有点后,最终ans逆序后存储的是字典序最小的方案,这是因为如果先遍历1号点(编号从1到n),则1号点一定最后被加入欧拉路径。

  • 需要注意的是,我们之前对图进行遍历的时候,是使用点进行判重,因为每个点只会被遍历一次,所以每条边也只会被遍历一次,因此时间复杂度是O(n+m)的。

  • 但是欧拉路径中我们需要对边进行判重,这样时间复杂度会很高。比如某个给定的图只有一个点,但是有很多自环,比如m条自环,然后使用bool数组存储每条边有没有被遍历过。根据上面伪码描述,第一次运行到dfs中会遍历m条边,递归进入dfs后还需要遍历m条边,一共需要遍历m次,因此m个m相加等于 m ∗ m = m 2 m*m = m^2 mm=m2,这个时间复杂度很高,一些题目可能会超时,因此需要优化。

  • 优化的方式是每次遍历过这条边后将其从邻接表中删除,保证之后不会再遍历到这条边。这样就可以保证每条边只会被遍历一次,时间复杂度变为线性。

  • 另外还要注意对于无向图,我们在建图的时候加了两条边,因为我们都是成对加入的边,比如(0,1)、(2,3)、…,我们知道一条边的编号是i,则另一条边的编号是i^1,因为无向图每条边也只能被用一次,如果某条边被用过了,另一条边也要标记一下不能使用了。

2. AcWing上的欧拉回路和欧拉路径题目

AcWing 1123. 铲雪车

问题描述

分析

  • 因为题目保证铲雪车从起点一定可以到达任何街道,这就意味着铲雪车必定是在某个街道上面。

  • 因为对于任意一条街道的两个端点中的任意一个端点而言,这个街道都会让其入度和出度加1,因此将此图看成有向图,则所有点的入度和出度都相等,因此必然存在欧拉回路。

  • 那么我们就可以计算一下所以街道的双向距离,最终除以速度就是答案。

代码

  • C++
#include <iostream>
#include <cmath>

using namespace std;

int main() {
    
    double x1, y1, x2, y2;

    cin >> x1 >> y1;
    double sum = 0;
    while (cin >> x1 >> y1 >> x2 >> y2) {
        double dx = x1 - x2, dy = y1 - y2;
        sum += sqrt(dx * dx + dy * dy) * 2;
    }
    
    int minutes = round(sum / 1000 / 20 * 60);
    int hours = minutes / 60;
    minutes %= 60;
    
    printf("%d:%02d\n", hours, minutes);  // 0表示不足的补0, 2表示保留两位
    
    return 0;
}

AcWing 1184. 欧拉回路

问题描述

分析

  • 我们只要清楚如何判断欧拉回路即可。

  • 对于无向图,如果所有边连通,且所有点的度数都是偶数则是欧拉回路。

  • 对于有向图,如果所有边连通,且所有点的入度等于出度即可。

  • 欧拉路径中我们需要对边进行判重,这样时间复杂度会很高。比如某个给定的图只有一个点,但是有很多自环,比如m条自环,然后使用bool数组存储每条边有没有被遍历过。根据上面伪码描述,第一次运行到dfs中会遍历m条边,递归进入dfs后还需要遍历m条边,一共需要遍历m次,因此m个m相加等于 m ∗ m = m 2 m*m = m^2 mm=m2,这个时间复杂度很高,本题会超时,因此需要优化。

  • 优化的方式是每次遍历过这条边后将其从邻接表中删除,保证之后不会再遍历到这条边。这样就可以保证每条边只会被遍历一次,时间复杂度变为线性。

  • 对于无向图,我们在建图的时候加了两条边,因为我们都是成对加入的边,比如(0,1)、(2,3)、…,我们知道一条边的编号是i,则另一条边的编号是i^1,因为无向图每条边也只能被用一次,如果某条边被用过了,另一条边也要标记一下不能使用了。

  • 写法参考网址:网址

代码

  • C++
#include <iostream>
#include <cstring>

using namespace std;

const int N = 100010, M = 400010;

int type;  // 1代表无向图,2代表有向图
int n, m;
int h[N], e[M], ne[M], idx;
bool used[M];  // 记录每条边是否被使用了
int ans[M / 2], 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) {
    
    while (~h[u]) {  // 这样写是为了跳过应遍历的边,h[u]存储的永远是我们可以使用的第一条边
        int i = h[u];  // 当前遍历第i条边
        
        if (used[i]) {  // 防止无向图的反向边被加入答案中
            h[u] = ne[i];
            continue;
        }
        
        h[u] = ne[i];  // 这里h[u]被更新,因为h是全局变量,下次dfs就不会遍历到当前考察的边
        used[i] = true;
        if (type == 1) used[i ^ 1] = true;
        
        dfs(e[i]);
        
        if (type == 1) {
            int t = i / 2 + 1;  // 无向图当前考察的是第t条边(从1开始)
            if (i & 1) t *= -1;
            ans[++cnt] = t;
        } else ans[++cnt] = i + 1;
    }
}

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);
        dout[a]++, din[b]++;  // 一条边只会让两个点的度增加,因此对于无向图也是成立的
    }
    
    // 判断是否无解
    if (type == 1) {
        for (int i = 1; i <= n; i++)
            if (din[i] + dout[i] & 1) {  // 成立的话说明点i的度为奇数
                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]) {  // 即h[i] != -1
            dfs(i);
            break;  // 只能执行一次dfs函数,否则说明其他边在其他连通分量中
        }
    
    if (cnt < m) {  // 说明所有边不连通
        puts("NO");
        return 0;
    }
    
    // 输出答案
    puts("YES");
    for (int i = cnt; i; i--) printf("%d ", ans[i]);
    puts("");
    
    return 0;
}

AcWing 1124. 骑马修栅栏

问题描述

分析

  • 将栅栏的交点看成顶点,栅栏看成边,相当于给我们给我们一个无向图,让我们求一个欧拉路径。

  • 题目要求输出字典序最小的一个,我们只需要保证从小到大考虑每个点即可。

  • 当我们按照从小到大的顺序遍历所有点后,最终ans逆序后存储的是字典序最小的方案,这是因为如果先遍历1号点(编号从1到n),则1号点一定最后被加入欧拉路径。

  • 另外我们还需要找到遍历的起点,首先要跳过孤立点,然后如果存在度数为奇数的点的话,从编号最小的奇数点开始dfs。

代码

  • C++
#include <iostream>
#include <cstring>

using namespace std;

const int N = 510;

int n = 500, m;  // n最大500, m边数
int g[N][N];  // g[i][j] = k,表示i到j有k条栅栏
int ans[1100], cnt;  // 记录欧拉路径,最多1024条边
int d[N];  // 记录每个点的度

void dfs(int u) {
    
    for (int i = 1; i <= n; i++)
        if (g[u][i]) {
            g[u][i]--, g[i][u]--;
            dfs(i);
        }
    ans[++cnt] = u;
}

int main() {
    
    cin >> m;
    for (int i = 0; i < m; i++) {
        int a, b;
        cin >> a >> b;
        g[a][b]++, g[b][a]++;
        d[a]++, d[b]++;
    }
    
    int start = 1;
    while (!d[start]) start++;  // 跳过孤立点
    for (int i = 1; i <= n; i++)
        if (d[i] % 2) {  // 题目保证一定有答案,如果存在度为奇数的点的话,只能存在2个
            start = i;
            break;
        }
    
    dfs(start);
    
    for (int i = cnt; i; i--) printf("%d\n", ans[i]);
    
    return 0;
}

AcWing 1185. 单词游戏

问题描述

分析

  • 这一题的建图方式和AcWing 1165. 单词环是一模一样的,每个单词看成一条边,首字母和为字母看成图中的顶点。这是一个有向图。

  • 建完图之后,问题就变成了我们能否找到一条路径,依次走过每条边,即该有向图中是否存在欧拉路径?

  • 我们要清楚如何判有向图是否存在欧拉路径,需要满足两个条件:

    (1)所有的边要连通;

    (2)除了起点和终点外,其余点的入度必须等于出度;

  • 对于(1),可以使用并查集解决,如果某个点有边相连,但是和并查集不连通,说明边不连通。

  • 本题不需要将所有边存储下来,只需要判断连通性以及记录每个点的入度和出度判断是否存在答案即可。

代码

  • C++
#include <iostream>
#include <cstring>

using namespace std;

const int N = 30;  // 点数最多26个

int n;  // 单词数目,图中对应边数
int p[N];  // 并查集
int din[N], dout[N];
bool st[N];  // 记录某个字母是否作为单词首尾出现过

int find(int x) {
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}

int main() {
    
    char str[1010];
    
    int T;
    scanf("%d", &T);
    while (T--) {
        
        cin >> n;
        
        memset(din, 0, sizeof din);
        memset(dout, 0, sizeof dout);
        memset(st, 0, sizeof st);
        for (int i = 0; i < 26; i++) p[i] = i;
        
        for (int i = 0; i < n; i++) {
            scanf("%s", str);
            int a = str[0] - 'a', b = str[strlen(str) - 1] - 'a';
            st[a] = st[b] = true;
            dout[a]++, din[b]++;
            p[find(a)] = find(b);
        }
        
        // 判断度数是否正确
        int start = 0, end = 0;
        bool success = true;  // 是否存在欧拉路径
        for (int i = 0; i < 26; i++) 
            if (din[i] != dout[i]) {
                if (din[i] == dout[i] + 1) end++;  // 终点数目,只能有1个
                else if (dout[i] == din[i] + 1) start++;  // 起点数目,只能有一个
                else {
                    success = false;
                    break;
                }
            }
        // 判断起点终点数目是否正确
        if (success && !(!start && !end || start == 1 && end == 1)) success = false;
        
        // 判断所有边是否连通
        int rep = -1;  // 代表元素
        for (int i = 0; i < 26; i++)
            if (st[i]) {  // 说明该字母在单词中出现过,所有st为true的都应该在一个集合中
                if (rep == -1) rep = find(i);
                else if (rep != find(i)) {  // 说明存在边不连通
                    success = false;
                    break;
                }
            }
        
        if (success) puts("Ordering is possible.");
        else puts("The door cannot be opened.");
    }
    
    return 0;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值