术语
- 欧拉路径(Eulerian trail):图中恰好将所有边都访问一次的路径;
- 欧拉回路(Eulerian cycle):开始节点和结束节点相同的欧拉路径,也称欧拉环;
- 欧拉图:具有欧拉回路的图;
- 半欧拉图:有欧拉路径但没有欧拉环的图;
存在性定理(度判定)
对于无向图:奇度个数为0或2!!
- 欧拉图的充要条件:奇度节点个数为0;
- 半欧拉图的充要条件:奇度节点个数为2;
- 存在欧拉路径的充要条件:奇度节点个数为0或2;
对于有向图:出入度相等或各有一个为+1和-1!!(假设出度记为正,入度记为负,出度入度和记做节点的度)
- 欧拉图的充要条件:所有节点出度等于入度,即所有节点度为0;
- 半欧拉图的充要条件:存在一个节点度为1,另一个节点度为-1,其余节点度为0;
- 存在欧拉路径的充要条件:所有节点度为0,或者只有两个节点不为0,且其中一个节点度为1另一个节点度为-1;
获取欧拉路径
- 问题:判断给定图中是否存在欧拉路径,如果存在则返回其中一条欧拉路径
- 分析:获取欧拉路径的一般思路:
- 根据存在性条件判断图中是否存在欧拉路径,并获取遍历入口;
- 如果是欧拉图,遍历可以从任意节点出发;
- 如果是半欧拉图,遍历必须从例外的两个节点出发;
- 调用以下算法,返回欧拉路径
Fleury算法:
算法思想:$v0$为遍历起点,令$P_0=v_0$;设$P_i=v_0e_1v_1e_2…e_i$ $v_i$已经访问,按下面方法从中选取$e{i+1}$:
1)$e{i+1}$与$v_i$相关联;
2)只有当无别的边可供访问时,才可以选择Gi=G-{e1,e2, …, ei}中的桥(所谓桥是一条删除后使连通图不再连通的边)作为$e{i+1}$;
3)当(2)不能再进行时,算法停止;
解释:我们使用栈来存储当前行走中的路径,假设当前栈顶为[…,u,v],如果栈顶节点v还有未访问的边,我们就选择其中一条边的顶点w进行深度遍历并将遍历到的顶点压入栈中,同时删除经过的边,如果栈顶节点v所有边都已被访问,说明v的后续分量中的边都已被访问,边(u,v)就变成了连接v的后续分量的桥(没办法回来了),直接将v出栈输出,此时还要查看顶点u是否有别的未访问的边,只有当u其余未访问的边都被输出之后(必然会再次回到u,因为v后续分量必然含有奇度节点,初始节点也是奇度节点,如果不回到u,说明u后还有其他奇度节点,矛盾),才能输出(u,v)边,这样最后输出的顶点逆序就是一个欧拉路径(欧拉回路的证明更简单,略)。
算法实现:
1 | def fleury(graph): |
- 算法分析:由于算法要经过每条边,所以时间必然是Ω(E)。在最坏情况下,在每个节点处进行一次 DFS,节点会重复走所以以边计算,算法复杂度应该是 O(E(E+V))
Hierholzer算法
算法思想(以半欧拉图为例):以一个奇度节点出发进行深度遍历,遍历的同时将经过的边删除(过河拆桥),如果某个节点不能继续遍历,则输出该节点。当遍历到u节点时,从该节点出发的深度遍历有两种结果:
- 1)要么重新返回该节点(子环),如果该节点还有其他未访问的边,继续沿着该边深度遍历,仍有两种遍历结果,如果该顶点没有其他未访问的边,则可将环中的顶点输出
- 2)要么在某个节点w处无法继续遍历,此时其所有邻接边都已访问,假设到达w的边为(v,w),此时(v-w)必然是w后分量的桥,因为u到w遍历过程必然包含了奇度节点,所以其他的遍历过程必然回到出发点,这就允许我们先从v开始得到在其他环中的欧拉路径后,再回到v,输出(v,w)边,输出w,最终得到的序列逆序即为一条欧拉路径。
算法实现:有边过河拆桥,无边输出
1 | def hierholzer(graph): |
- 算法分析:时间复杂度是 O(E),其在 DFS 的过程中不用恢复边,靠出栈记录轨。代码简洁,效率又高!!