先判定有向欧拉路存在条件,再把每个点的出边按升序走 Hierholzer,最后逆序得到字典序最小的欧拉路径。
OJ: luogu
题目 ID: P7771
难度:普及+/提高
标签:图论欧拉路贪心模板题
日期: 2026-06-19 23:56
题意
给一张有向图,要求输出一条字典序最小的欧拉路径。
也就是:
- 每条边恰好走一次
- 输出经过的顶点序列
- 如果不存在这样的路径,输出
No
样例图
这张图展示样例 2 的结构:
digraph G {
rankdir=LR;
1 -> 2;
2 -> 3;
3 -> 4;
4 -> 3;
3 -> 5;
}
从 3 出发时,既可以去 4,也可以去 5。
为了让最终顶点序列字典序最小,应该优先走向编号更小的 4,于是答案是 1 2 3 4 3 5。
这说明除了判定欧拉路存在,还要额外处理“出边按什么顺序走”。
思路
先看一个小数据暴力:
cpp
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 10;
const int MAXM = 20;
int n, m;
int eu[MAXM], ev[MAXM];
vector<int> out_edges[MAXN];
bool used[MAXM];
vector<int> cur_path, best_path;
bool better(const vector<int> &a, const vector<int> &b) {
if (b.empty()) {
return true;
}
for (int i = 0; i < (int) a.size(); i++) {
if (a[i] != b[i]) {
return a[i] < b[i];
}
}
return false;
}
void dfs(int u, int used_cnt) {
if (used_cnt == m) {
if (better(cur_path, best_path)) {
best_path = cur_path;
}
return;
}
for (int id : out_edges[u]) {
if (used[id]) {
continue;
}
used[id] = true;
cur_path.push_back(ev[id]);
dfs(ev[id], used_cnt + 1);
cur_path.pop_back();
used[id] = false;
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
out_edges[i].clear();
}
for (int i = 1; i <= m; i++) {
cin >> eu[i] >> ev[i];
out_edges[eu[i]].push_back(i);
used[i] = false;
}
for (int i = 1; i <= n; i++) {
sort(out_edges[i].begin(), out_edges[i].end(), [&](int x, int y) {
if (ev[x] != ev[y]) {
return ev[x] < ev[y];
}
return x < y;
});
}
best_path.clear();
for (int start = 1; start <= n; start++) {
cur_path.clear();
cur_path.push_back(start);
dfs(start, 0);
}
if (best_path.empty()) {
cout << "No\n";
} else {
for (int i = 0; i < (int) best_path.size(); i++) {
if (i) {
cout << ' ';
}
cout << best_path[i];
}
cout << '\n';
}
return 0;
}暴力直接枚举所有可能的走法:
- 从每个点尝试作为起点
- 每次任选一条还没用过的出边继续走
- 如果最后恰好用完全部边,就得到一条候选欧拉路径
- 在这些候选里取字典序最小
这个办法只能验证小数据,正式做法要用 Hierholzer。
先回顾有向欧拉路的存在条件:
- 把有向边看成无向边后,所有有边的点要连通
- 度数满足以下两种之一:
- 所有点
in = out,存在欧拉回路 - 恰有一个点满足
out = in + 1,作为起点 - 恰有一个点满足
in = out + 1,作为终点
- 所有点
有了解的判定后,再考虑字典序。
Hierholzer 的特点是:
- 走边时先不输出
- 回溯时再把点放进答案
- 所以答案会倒着产生
为了让最终答案字典序最小,只要让每个点总是优先走向编号更小的后继即可。
实现上:
- 每个点的出边按终点升序排序
- 用栈模拟 Hierholzer
- 当前点还有边,就沿最小的那条边继续走
- 当前点没边了,就把它放进
path - 最后把
path倒着输出
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005;
int n, m;
vector<int> graph[MAXN];
vector<int> weak[MAXN];
int indeg[MAXN], outdeg[MAXN], iter_[MAXN];
vector<int> path;
bool check_connected() {
int start = 0;
for (int i = 1; i <= n; i++) {
if (indeg[i] + outdeg[i] > 0) {
start = i;
break;
}
}
if (start == 0) {
return true;
}
vector<int> vis(n + 1, 0);
stack<int> st;
st.push(start);
vis[start] = 1;
while (!st.empty()) {
int u = st.top();
st.pop();
for (int v : weak[u]) {
if (!vis[v]) {
vis[v] = 1;
st.push(v);
}
}
}
for (int i = 1; i <= n; i++) {
if (indeg[i] + outdeg[i] > 0 && !vis[i]) {
return false;
}
}
return true;
}
int find_start() {
int start_cnt = 0, end_cnt = 0;
int start = 0;
for (int i = 1; i <= n; i++) {
if (outdeg[i] == indeg[i] + 1) {
start_cnt++;
start = i;
} else if (indeg[i] == outdeg[i] + 1) {
end_cnt++;
} else if (indeg[i] != outdeg[i]) {
return -1;
}
}
if (start_cnt == 1 && end_cnt == 1) {
return start;
}
if (start_cnt == 0 && end_cnt == 0) {
for (int i = 1; i <= n; i++) {
if (outdeg[i] > 0) {
return i;
}
}
return 1;
}
return -1;
}
void hierholzer(int start) {
vector<int> st;
st.push_back(start);
while (!st.empty()) {
int u = st.back();
if (iter_[u] < (int) graph[u].size()) {
int v = graph[u][iter_[u]++];
st.push_back(v);
} else {
path.push_back(u);
st.pop_back();
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
graph[i].clear();
weak[i].clear();
indeg[i] = outdeg[i] = iter_[i] = 0;
}
for (int i = 1; i <= m; i++) {
int u, v;
cin >> u >> v;
graph[u].push_back(v);
weak[u].push_back(v);
weak[v].push_back(u);
outdeg[u]++;
indeg[v]++;
}
for (int i = 1; i <= n; i++) {
sort(graph[i].begin(), graph[i].end());
}
if (!check_connected()) {
cout << "No\n";
return 0;
}
int start = find_start();
if (start == -1) {
cout << "No\n";
return 0;
}
path.clear();
hierholzer(start);
if ((int) path.size() != m + 1) {
cout << "No\n";
return 0;
}
for (int i = m; i >= 0; i--) {
cout << path[i];
if (i > 0) {
cout << ' ';
}
}
cout << '\n';
return 0;
}复杂度
设点数为 n,边数为 m。
- 判连通和度数检查是
- 邻接表排序总复杂度
量级 - Hierholzer 主过程是
总时间复杂度
总结
这题就是“有向欧拉路 + 字典序”模板。难点不在 Hierholzer 本身,而在两件事:
- 先把欧拉路存在条件判对
- 再利用“答案逆序产生”这个性质,把出边顺序处理成最终的最小字典序
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
