用边编号 + 链式邻接表实现迭代版 Hierholzer 求欧拉路,出栈序列逆序输出。
OJ: roj
题目 ID: 1341
难度:普及
标签:
创建: 2026-09-30 06:09
更新: 2026-10-05 11:08
题目描述
一笔画的路径叫欧拉路,最后回到起点的叫欧拉回路。给定 5 5 与边 1 5 4 3 2 1。
思路
奇度点个数为
参考代码
Python 版:
python
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-09-30 06:09
# update_at: 2026-09-30 06:12
import sys
from collections import defaultdict
def euler_path(adj: dict[int, list[tuple[int, int]]], deg: list[int], m: int) -> list[int]:
"""Hierholzer 迭代版:一路贪心走到底,走不动才把点记进路径;出栈顺序即答案。
每条无向边各占一个编号(两端都能注销它),故 1-2 之间的平行边不会被漏掉。
邻居按号码倒排后从栈顶取,每次便拿到最小邻居,输出与参考解一致。
"""
used = [False] * m # used[e]:第 e 条边已被走掉
odd = [v for v in range(1, len(deg)) if deg[v] % 2]
start = max(odd) if odd else 1 # 有奇点就从最大奇点出发,否则任取一点
path: list[int] = []
stack = [start]
while stack:
v = stack[-1]
while adj[v] and used[adj[v][-1][1]]: # 栈顶是已走过的边就弹掉
adj[v].pop()
if adj[v]:
w, e = adj[v].pop() # 贪心走向还没用过的邻居
used[e] = True
stack.append(w)
else:
path.append(stack.pop()) # 无路可走,这个点正式落进路径
return path # 回溯顺序自后向前拼接,无需再反转
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n, m = next(data), next(data)
adj: dict[int, list[tuple[int, int]]] = defaultdict(list)
deg = [0] * (n + 1)
for e in range(m):
a, b = next(data), next(data)
adj[a].append((b, e)) # 存 (邻居, 边编号)
adj[b].append((a, e))
deg[a] += 1
deg[b] += 1
for v in adj:
adj[v].sort(key=lambda item: -item[0]) # 倒排,使栈顶 pop 出的是最小邻居
print(' '.join(map(str, euler_path(adj, deg, m))))
if __name__ == "__main__":
solve()C++ 版(同一算法):
cpp
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-05 10:46
* update_at: 2026-10-05 11:05
*/
#include <cstdio>
typedef long long ll;
const int MAXN = 1005; // 点数上限
const int MAXM = 2005; // 边数上限
// 无向图:每条边存两个方向的半边,用边编号标记是否走过(正确处理平行边)
struct Edge {
ll to; // 这条半边指向的点
ll id; // 所属无向边的编号
};
Edge edge[MAXM * 2]; // 邻接表:每条无向边存两条半边
ll head[MAXN]; // head[u]:u 的邻接表链头(-1 表示空)
ll nxt[MAXM * 2]; // 链表 next 指针
ll cnt_edge = 0; // 半边计数器
bool used[MAXM]; // used[e]:第 e 条无向边是否已经走过
ll sta[MAXM + 10]; // 手写栈:沿途的点
ll top = 0; // 栈顶指针
ll ans[MAXM + 10]; // 出栈序列(逆序输出后就是欧拉路)
ll len_ans = 0; // 出栈序列长度
ll deg[MAXN]; // 每个点的度数,用来找奇点
// 加一条无向边,两个方向各存一条半边
void add_edge(ll u, ll v, ll id) {
edge[cnt_edge].to = v; edge[cnt_edge].id = id;
nxt[cnt_edge] = head[u];
head[u] = cnt_edge; cnt_edge++;
edge[cnt_edge].to = u; edge[cnt_edge].id = id;
nxt[cnt_edge] = head[v];
head[v] = cnt_edge; cnt_edge++;
}
// 检查第 e 条半边是否可走:它所属的无向边(含反向半边)还没走过
bool check_ok(ll e) {
if (used[edge[e].id]) return false;
if (used[edge[e ^ 1].id]) return false;
return true;
}
// Hierholzer 迭代版:能走就走并压栈,走不动才把点记进出栈序列
void solve() {
ll n, m;
scanf("%lld %lld", &n, &m);
for (ll i = 0; i < m; ++i) {
ll a, b;
scanf("%lld %lld", &a, &b);
add_edge(a, b, i);
deg[a]++; deg[b]++;
}
// 选起点:有奇点就从编号最大的奇点出发,否则取第一个有边的点
ll start = 0;
for (ll i = 1; i <= n; ++i)
if (deg[i] % 2 == 1) start = i;
if (start == 0)
for (ll i = 1; i <= n; ++i)
if (deg[i] > 0) { start = i; break; }
if (start == 0) start = 1; // 一条边都没有时兜底输出 1
sta[top++] = start;
while (top > 0) {
ll u = sta[top - 1]; // 只看栈顶,不弹出
ll e = head[u];
while (e != -1 && !check_ok(e)) e = nxt[e]; // 找第一条可走的边
if (e != -1) {
used[edge[e].id] = true; // 走掉这条边
sta[top++] = edge[e].to;
} else {
top--; // 无路可走,这个点正式落进出栈序列
ans[len_ans++] = u;
}
}
// 出栈序列逆序输出才是欧拉路(起点在前,终点在后)
for (ll i = len_ans - 1; i >= 0; --i)
printf("%lld%c", ans[i], i == 0 ? '\n' : ' ');
}
int main() {
for (ll i = 0; i < MAXN; ++i) head[i] = -1;
solve();
return 0;
}