欧拉回路/欧拉路径

本文介绍了欧拉回路和欧拉路径的概念,强调了它们在图论中的性质,并详细讲解了Hierholzer算法用于寻找欧拉回路的方法。还涉及了如何在实际问题中应用这些概念,如骑马修栅栏和单词游戏中的路径问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

定义

欧拉回路:指在一个起点出发,能经过图中每条边恰好一次且能回到起点的回路

欧拉路径:和欧拉回路类似,不同的是不回到起点。

性质

欧拉回路:

在有向图中,如果存在欧拉回路,那么每个点入度和出度相同。

在无向图中,如果存在欧拉回路,那么每个点的度都为偶数。

欧拉路径:

在有向图中,如果存在欧拉路径,那么会存在一个入度大于出度恰好为一的点,一个出度大于入度恰好为一的点,其余的点的度都为偶数。

在无向图中,如果存在欧拉路径,那么有两个点度数为奇数,其余的点的度都为偶数。

寻找欧拉回路/路径的算法——Hierholzer算法

该算法的思想是找到一个回路后,任取回路的一个点替换为一个简单回路,直到无法再替换为止。

如果要寻找欧拉路径,则需要从起点开始进行算法。

相关题目

欧拉回路

在这里插入图片描述

思路

模板题,先根据欧拉回路的性质判断是否满足,然后从一个度数不为 0 0 0 的点进行 H i e r h o l z e r Hierholzer Hierholzer 算法,如果能遍历到所有边则存在欧拉回路,否则则不存在。(因为图可能不联通)

代码实现

在代码实现中,当找到一个环后,要把环上的边从边集中“删去”,或者说打个标记,表示这条边已经遍历过了。

对于无向边,把无向图的无向边当做两条互反的有向边处理,所以当其中一条有向边“删去”时,要把另外一条也“删去”。

为了快速找到反边,使用链式前向星的写法,建无向边时,将无向边对应的两条有向边挨个插入,使得这两条边的的下标为相邻的一对奇偶数,就可通过一边下标异或一得到另外一条边的下标。

为了保证复杂度是 O ( m ) O(m) O(m) 的,每遍历完一条边,要将邻接表的表头更新为下一条边,这样就不会在多次遍历领接表时遍历到已遍历的边。

#include <bits/stdc++.h>
using namespace std;

const int N = 4e5 + 5;

#define int long long

int n, m, t;
// h[i]为点i的邻接表表头
int h[N], e[N], v[N], id[N], ne[N], st[N], idx;

int d[N], in[N], out[N];

vector<int> ans;

// y为边的符号,正边为1,反边为-1
void add(int a, int b, int x, int y)
{
    e[idx] = b, v[idx] = x, id[idx] = y, ne[idx] = h[a], h[a] = idx++;
}

void dfs(int u)
{
    for (int i = h[u]; i != -1; i = h[u]) {
        if (st[i]){
            h[u] = ne[i];
            // 将邻接表表头更新
            continue;
        }
        st[i] = 1;
        // 标记该边已经走过
        if (t == 1) {
            st[i ^ 1] = 1;
        }
        h[u] = ne[i];
        // 更新表头
        dfs(e[i]);
        ans.push_back(id[i]*v[i]);
        // 因为是在点e[i]拓展出去替换一个简单环进来
        // 所以边i到等替换完(dfs(e[i]回溯)才能加入
    }
}

signed main()
{
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    
    cin >> t >> n >> m;

    memset(h, -1, sizeof(h));
    for (int i = 1; i <= m; i++) {
        int a, b;
        cin >> a >> b;
        if (t == 1) {
            add(a, b, 1, i);
            add(b, a, -1, i);
            d[a]++, d[b]++;
        } else {
            add(a, b, 1, i);
            in[b]++, out[a]++;
        }
    }
    int flag = 1;
    if (t == 1) {
        for (int i = 1; i <= n; i++) {
            if (d[i] % 2)
                flag = 0;
        }
    } else {
        for (int i = 1; i <= n; i++) {
            if (in[i] != out[i])
                flag = 0;
        }
    }
    int sx = 1;
    for (int i = 1; i <= n; i++) {
        if(h[i]!=-1){
            sx = i;
            break;
        }
    }
    dfs(sx);
    // 图可能不连通,遍历完边数不为m
    if(ans.size()!=m){
        flag = 0;
    }
    if(!flag){
        cout<<"NO\n";
    }else{
        cout << "YES\n";
        // 因为是倒序加边,最后要倒回来
        reverse(ans.begin(),ans.end());
        for (int i : ans) {
            cout << i << ' ';
        }
    }
}

骑马修栅栏

在这里插入图片描述

思路

注意到点不多,可以直接开领接矩阵。
因为要求路径的字典序最小,所以从最小的起点开始出发。位于点 u u u 时,优先选择最小的点 v v v,从而保证字典序最小。

代码实现

#include <bits/stdc++.h>
using namespace std;

const int N = 5e2 + 5;

int m;
int d[N];
int g[N][N];
vector<int> ans;
void dfs(int u)
{
    for (int v = 1; v < N; v++) {
        if (g[u][v]) {
            g[u][v]--, g[v][u]--;
            dfs(v);
        }
    }
    ans.push_back(u);
}

int main()
{
    cin >> m;

    for (int i = 1; i <= m; i++) {
        int a, b;
        cin >> a >> b;
        d[a]++, d[b]++, g[a][b]++, g[b][a]++;
    }
    int sx = N;
    for (int i = 1; i < N; i++) {
        if (d[i]) {
            // 如果是欧拉路则找到较小的奇度点作为起点
            if (d[i] % 2) {
                sx = i;
                break;
            } else {
                // 否则找最小的点为起点
                sx = min(sx, i);
            }
        }
    }
    dfs(sx);
    reverse(ans.begin(), ans.end());
    for (int i : ans) {
        cout << i << '\n';
    }
}

单词游戏

在这里插入图片描述

思路

可以把每个字母看做一个点,每个单词为一条边,从单词的首字母向单词的末字母建一条有向边,然后判断是否满足欧拉回路\路径的性质,且跑出的边是恰好遍历所有边即可。

代码实现

#include <bits/stdc++.h>
using namespace std;

const int N = 5e2 + 5;

int m;
int cnt;
multiset<int> h[26];
int in[25], out[26];

void dfs(int u)
{
    for (auto it = h[u].begin(); it != h[u].end(); it = h[u].begin()) {
        int v = *it;
        h[u].erase(it);
        cnt++;
        dfs(v);
    }
}

void solve()
{
    for (int i = 0; i < 26; i++) {
        h[i].clear();
        in[i] = out[i] = 0;
    }

    cin >> m;
    for (int i = 1; i <= m; i++) {
        string s;
        cin >> s;
        int a = s[0] - 'a';
        int b = s.back() - 'a';
        h[a].insert(b);
        in[b]++, out[a]++;
    }

    int flag = 1, sx = -1, c1 = 0, c2 = 0;
    for (int i = 0; i < 26; i++) {
        if (in[i] + 1 == out[i]) {
            sx = i;
            c1++;
        } else if (in[i] == out[i] + 1) {
            c2++;
        } else if (in[i] != out[i]) {
            flag = 0;
        }
    }
    if (sx == -1) {
        for (int i = 0; i < 26; i++) {
            if (h[i].size()) {
                sx = i;
            }
        }
    }
    if (c1 == c2 && c1 <= 1) {
        cnt = 0;
        dfs(sx);
        if (cnt != m)
            flag = 0;
    } else {
        flag = 0;
    }
    if (flag)
        cout << "Ordering is possible.\n";
    else
        cout << "The door cannot be opened.\n";
}

int main()
{
    int T;
    cin >> T;
    while (T--) {
        solve();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值