P7771 【模板】欧拉路径
- Upd on 2022.8.22 改正笔误。
P7771 【模板】欧拉路径。
摘自学习笔记 初级图论 第六章。
建议在博客内阅读。
6.1 定义与判定
- 欧拉路径:通过 连通图 中所有边恰好一次的路径称为欧拉路径。
- 欧拉回路:通过 连通图 中所有边恰好一次的 回路 称为欧拉回路。回路,即路径起点和终点相同。
- 欧拉图:具有欧拉 回路 的有向图或无向图称为欧拉图。
- 半欧拉图:具有欧拉 通路 但 不具有欧拉回路 的有向图或无向图称为半欧拉图。
做题时基本用不到上述定义(大雾)。
不严谨地说,欧拉图能够从任意一点作为起点一笔画完整张图然后回到该点,而半欧拉图只能从某两个点
欧拉图的判定:一张无向图
考虑证明上述结论。
无论是对于无向图还是有向图而言,必要性都是显然的。考虑最终欧拉回路的形态,每次进入一个点,都要从该点走出去,所以出边和入边必须两两抵消。对于起始点,它一开始走出去的边和最后走回它的边同样抵消了,所以有向图存在欧拉回路必须满足每个点出度和入度相等。同理可证无向图每个点的度数必须为偶数。
充分性通过构造法证明。考虑无向图,首先找到图上任何一个回路
半欧拉图的判定:一张无向图
6.2 求欧拉路径:Hierholzer 算法
Hierholzer 算法的核心是不断往当前回路的某个点中插入环,这和欧拉回路存在的判定条件的充分性证明如出一辙,或者说完全等价。
先考虑有向图吧,因为有向图不需要考虑重边的问题。
6.2.1 朴素方法
根据流程,我们有一个朴素的实现方法:首先 dfs 找到经过某个点的任意一个环
每次枚举到一个点时,我们都要遍历它的所有出边以找到第一个没有被删去的边,复杂度为
若用双向链表维护每个点剩余的所有出边,则每条边只会被遍历一次,时间复杂度
进一步地,我们发现每次删去的环边一定是每个节点所有没有被删去的出边中开头的若干个。也就是说,如果给
不断无脑深搜,最终一定能找到环。
证明该结论。不妨设我们从
因此,最终必然只可能在
根据上述结论,我们不需要维护双向链表了。存图用的链式前向星可以满足我们的要求,因为我们每次只会删去开头的出边。这样改进后,链表头就和网络最大流 Dinic 算法的当前弧非常相似,都是 记录第一个有用的边 以省去一条条跳无用边的时间。
该结论同时也证明了找到一个环的复杂度关于环上边数线性,所以总复杂度即
注意,需要使用双向链表维护将一个环插入当前回路的过程,否则复杂度会退化成
6.2.2 巧妙方法
上述做法的时间复杂度已经足够优秀,但实现起来稍微有些复杂。我们希望算法能够更简单。
我们可以用自己的语言描述复杂的方法干了些什么,再思考有哪些地方可以简化。
从整体上考察,我们无非就是实现了这样的步骤:从一个起点开始找到一个环,然后以环上的每个起点开始找到一个环 …… 不断递归下去。
然后思考究竟是哪里麻烦了。我们会发现,为了依次从环上的每个起点开始找环,我们需要先 显式 地将这个环找到,排出来,再依次处理上面的所有节点。所以我们需要一个 dfs 函数找环,另一个递归式函数解决欧拉回路问题。
这样太蠢了,因为无论以怎样的顺序安排环上节点的找环顺序,都不会影响欧拉回路的正确性。我们只是找环并插入啊,换个顺序又不会让问题变得无解。
因此,我们直接在 dfs 找环的回溯过程中,直接对环上的每个节点找环。换句话说,我们将原来找环的顺序倒过来,这样我们就没有必要先显式地找到当前环了,而是在回溯的过程中,一边对当前点找环,一边往回路中插入当前环。
综上,我们得到了求解欧拉回路的最常用算法 —— Hierholzer 算法的具体步骤:遍历当前节点
如果要求字典序最小,只需在一开始对每个点的所有出边从小到大排序。这样一来,欧拉回路上从左往右看,每个点的后继都取到了理论最小值。
6.2.3 无向图和欧拉通路
以上,我们通过两小节的篇幅提出并优化了有向图欧拉回路的求解方法。
对于无向图的欧拉回路,我们可以类似有向图欧拉回路一样做,唯一的注意点是我们需要对边判重。使用求桥边时的成对变换技巧,用
对于无向图和有向图的欧拉通路,注意必须从奇点或唯一的出度大于入度的点开始 dfs。其它地方和欧拉回路没有区别。
模板题 P7771 【模板】欧拉路径 代码如下。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5;
int n, m, lar, top, stc[N], in[N], hd[N];
vector<int> e[N];
void dfs(int id) {
for(int &i = hd[id]; i < e[id].size(); ) dfs(e[id][i++]);
stc[++top] = id;
}
int main() {
cin >> n >> m;
for(int i = 1; i <= m; i++) {
int u, v;
scanf("%d%d", &u, &v);
e[u].push_back(v), in[v]++;
}
for(int i = 1; i <= n; i++) {
sort(e[i].begin(), e[i].end());
if(abs((int) e[i].size() - in[i]) > 1) puts("No"), exit(0);
if(e[i].size() > in[i])
if(lar) puts("No"), exit(0);
else lar = i;
}
dfs(lar ? lar : 1);
if(top != m + 1) puts("No");
else {
reverse(stc + 1, stc + top + 1);
for(int i = 1; i <= top; i++) cout << stc[i] << " ";
}
return 0;
}
6.3 参考文章
- 欧拉回路与欧拉通路存在性的充要条件及其证明。
- 欧拉图 - OI Wiki。