欧拉图
概念¶
- 欧拉回路:经过每条边刚好一次的回路
- 欧拉通路:经过每条边刚好一次的通路
- 欧拉图:具有欧拉回路的图
- 半欧拉图:具有欧拉通路但不具有欧拉回路的图
性质¶
- 欧拉图:
- 所有结点的度为偶数
- 欧拉图是若干个环的并,并且每条边必然处于奇数个环中
- 一个很不严谨的 Proof: 假设这条边是 \(e(u, v)\),那么对于每一个环,必然存在一条边 \(e^\prime(v^\prime, u)\) 与此条边对应,从 \(e^\prime\) 到结点 \(u\),走 \(e\) 离开 \(v\),如果它存在于偶数个环中,那么每个 \(e^\prime\) 都会对 \(\deg(u)\) 产生 1 个贡献,加上 \(e\) 的贡献那么 \(\deg(u)\) 必然是奇数,与上一条性质矛盾
判别¶
- 无向图
- 欧拉图
- 非零度结点连通
- 结点的度数都是偶数
- 半欧拉图
- 非零度结点连通
- 只有两个节点的度数为奇数
- 欧拉图
- 有向图
- 欧拉图
- 非零度结点强连通
- 结点入度等于出度
- 半欧拉图
- 非零度结点强连通
- 至多 1 个结点入度 - 出度 = 1
- 至多 1 个结点出度 - 入度 = 1
- 其余结点出度等于入度
- 欧拉图
求欧拉路/欧拉回路¶
一般使用 Hierholzer 算法,实质是 dfs 一个个求解单个环的过程,每次经过一条边就将其删除,dfs 完一个结点就将其弹入栈中,记录下这条边(从哪里进入该结点)。
void dfs(int u) {
// 使用邻接矩阵存储
for (int &v = cnt[u]; v < g[u].size(); ++v) { // 每次 ++v 相当于 ++cnt[u],避免重复访问 0 ~ cnt[u]-1 号结点
if (g[u][v]) {
g[u][v] = g[v][u] = 0;
dfs(v);
}
}
stk.push(u);
}
为什么一定要在回溯时将结点入栈
考虑下面这张图:
如果从 1 开始 dfs,那么从一开始就将 \(u\) 入栈会得到一个可能的欧拉路径 1 2 3 4 1
,而回溯时入栈会得到正确的欧拉路径 1 4 3 1 2
。
不难发现,如果在回溯时入栈,那么发生入栈操作只会是在找到最后一条完整的欧拉回路/欧拉路径的情况下才会发生,即便在走某条欧拉回路中途走了一段“岔路”也会先直接将这一段岔路入栈,然后走完了这条欧拉回路才完整地将整条回路入栈,避免了“断层”现象。