欧拉路径 & 欧拉回路

定义

  • 欧拉路径:经过连通图中所有边恰好一次的迹称为 欧拉路径

  • 欧拉回路:经过连通图中所有边恰好一次的回路称为 欧拉回路。即首尾相连的欧拉路径。

  • 欧拉图:具有欧拉回路的图称为 欧拉图

  • 半欧拉图:具有欧拉路径但不具有欧拉回路的图称为 半欧拉图

不经过重复边的途径称为

判定

  • 有向图欧拉路径:图中恰好存在 \(1\) 个点的出度比入度多 \(1\) (该点即为起点 \(S\)),\(1\) 个点的入度比出度多 \(1\) (该点即为终点 \(T\)),其余点的出度等于入度。

  • 有向图欧拉回路:图中所有点的入度等于出度(起点 \(S\) 和终点 \(T\) 可以为任意点)。

  • 无向图欧拉路径:图中恰好存在 \(2\) 个点的度数是奇数,其余点的度数为偶数,这两个度数为奇数的点即为欧拉路径的起点 \(S\) 和终点 \(T\)

  • 无向图欧拉回路:图中所有点的度数都是偶数(起点 \(S\) 和终点 \(T\) 可以为任意点)。

注意:存在欧拉路径的充要条件是无向图连通,有向图弱连通。

对于有向图的两点 \(u, v\) ,若将有向边改为无向边后 \(u, v\) 连通,则称 \(u, v\) 弱连通

构造

采用 Hierholzer 算法求解欧拉路径或欧拉回路。

具体地,遍历当前点 \(u\) 的所有出边 \(u \rightarrow v\)。若该边未走过,则向点 \(v\) DFS。遍历完所有出边后,将 \(u\) 加入回路。最终得到一条反向的欧拉回路,将其翻转即可。

代码实现上,有向弱连通图删边比标记边的 \(O(m^2)\) (其中 \(m\) 是边数)时间复杂度更优。可以用数组维护邻接矩阵,也可以用 vector 或链式前向星维护时间复杂度更优的邻接表。

欧拉路径的模版题为例,要求有向弱连通图中字典序最小的欧拉路径。由于点数 \(n\) 和边数 \(m\) 均是 \(10^5\) 级别的数据,领接矩阵的 \(O(n^2)\) 的时间复杂度不可接受。考虑采用类似链表的方式记录数组 \(nxt\),其中 \(nxt_u\) 表示 \(u\) 的邻接表中下一个访问的边的编号。初始时 \(nxt_u = 0\) 即可。考虑通过排序贪心地将点的编号越小的点排在最前面,最终求得的一定是字典序最小的欧拉路径。以下代码的时间复杂度为 \(O(n + m \log m)\)

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 8;
int n, m, in[N], out[N], nxt[N];
vector<int> e[N];
stack<int> stk;
void dfs(int u) {
    for (int i = nxt[u]; i < e[u].size(); i = nxt[u]) {
        nxt[u] = i + 1;
        dfs(e[u][i]);
    }
    stk.push(u);
}
int main() {
    cin >> n >> m;
    for (int i = 1, u, v; i <= m; i++) {
        cin >> u >> v;
        e[u].push_back(v);
        out[u]++; in[v]++;
    }
    for (int i = 1; i <= n; i++) sort(e[i].begin(), e[i].end());
    int cir = 1, cnts = 0, cntt = 0, s = 1;
    for (int i = 1; i <= n; i++)
        if (in[i] != out[i]) {
            cir = 0;
            if (in[i] - out[i] == 1) cntt++;
            else if (out[i] - in[i] == 1) s = i, cnts++;
            else {
                cout << "No";
                return 0;
            }
        }
    if (!cir && (cnts != 1 || cntt != 1)) {
        cout << "No";
        return 0;
    }
    dfs(s);
    while (!stk.empty()) {
        cout << stk.top() << " ";
        stk.pop();
    }
    return 0;
}

例题

词链

从每个字符的第一个字符向最后一个字符连边,以每条边对应的字符串的字典序排序,跑有向图欧拉路径。时间复杂度为 \(O(S \log S)\),其中 \(S = \sum \lvert {s_i} \rvert\)

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 8;
int in[N >> 3], out[N >> 3], n, cnt;
bool flag, vis[N];
string a[N], now[N], ans[N];
void dfs(int last, int step) {
    if (flag) return;
    if (step == n) {
        flag = true;
        for (int i = 1; i <= cnt; i++) ans[i] = now[i];
        return;
    }
    for (int i = 1; i <= n; i++) {
        if (vis[i]) continue;
        if (a[last][a[last].size() - 1] == a[i][0]) {
            vis[i] = true;
            now[++cnt] = a[i];
            dfs(i, step + 1);
            vis[i] = false;
            --cnt;
        }
    }
}
int main() {
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        out[a[i][0]]++;
        in[a[i][a[i].size() - 1]]++;
    }
    sort(a + 1, a + n + 1);
    char s, t;
    for (char c = 'a'; c <= 'z'; c++) {
        if (in[c] - out[c] == 1) t = c;
        else if (out[c] - in[c] == 1) s = c;
    }
    int start = 1;
    for (int i = 1; i <= n; i++)
        if (a[i][0] == s && (a[i][a[i].size() - 1] != t || in[t] > 1)) {
            start = i;
            break;
        }
    now[++cnt] = a[start];
    vis[start] = true;
    dfs(start, 1);
    if (!flag) {
        cout << "***";
        return 0;
    }
    for (int i = 1; i <= n; i++) {
        cout << ans[i];
        if (i != n) cout << '.';
    }
    return 0;
}

反色刷

考虑单个连通块,若它不存在欧拉回路,则该连通块无解。考虑多个连通块,只要有一个连通块无解,则该图无解。若连通块不是孤点(即有黑边)贡献为 \(1\),否则为 \(0\)

用并查集维护连通块黑边的数量和对答案的贡献。由上分析知,一个图有解等价于每个点的度数都是偶数。
启示:只需记录每个节点度数的奇偶性,维护度数之和,只要为 \(0\) 就有解。

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 8;
struct Edge { int u, v, c; } e[N];
int fa[N], dg[N], cnt[N], n, m, q, now;
int find(int x) { return fa[x] == x ? x : fa[x] = find(fa[x]); }
void merge(int x, int y) { fa[find(x)] = find(y); }
void update(int u, int v) {
    now -= dg[u] + dg[v];
    dg[u] ^= 1; dg[v] ^= 1;
    now += dg[u] + dg[v];
}
int read() {
    char ch = getchar(); int num = 0;
    while (ch < '0' || ch > '9') ch = getchar();
    while (ch >= '0' && ch <= '9') { num = (num << 1) + (num << 3) + ch - '0'; ch = getchar(); }
    return num;
}
int main() {
    n = read(); m = read();
    for (int i = 1; i <= n; i++) fa[i] = i;
    for (int i = 1, u, v, c; i <= m; i++) {
        u = read(), v = read(), c = read();
        e[i] = {u, v, c};
        if (c == 1) update(u, v);
        merge(u, v);
    }
    int ans = 0;
    for (int i = 1; i <= m; i++)
        if (e[i].c == 1) {
            int f = find(e[i].u);
            if (++cnt[f] == 1) ans++;
        }
    q = read();
    while (q--) {
        int op; op = read();
        if (op == 1) {
            int x; x = read();
            x++;
            update(e[x].u, e[x].v);
            int f = find(e[x].u);
            if (e[x].c == 1) {
                e[x].c = 0;
                if (--cnt[f] == 0) ans--; 
            } else {
                e[x].c = 1;
                if (++cnt[f] == 1) ans++;
            }
        } else cout << (now == 0 ? ans : -1) << "\n";
    }
    return 0;
}
posted @ 2025-11-08 18:03  zheyutao  阅读(12)  评论(0)    收藏  举报