图论 - 欧拉路径与欧拉回路

一、基本定义

欧拉路径

欧拉路径是指经过图中每一条边且仅能经过一次的路径,起点和终点可以不同。

想象你是一个邮递员,要走遍一座城市中的每一条街道,但每条街道只能走一次,这样的路线就是欧拉路径。开始和结束的位置可以不一样。

欧拉回路

欧拉回路是一条经过图中每一条边且仅能经过一次的路径,且起点和终点必须相同。

同样地,假设你是邮递员,你依旧需要走遍每一条街道,并且每一条街道仍只能走一次,但这次你要从某个地方出发,又回到这个地方,这样的路线就是欧拉回路。

二、判定条件

对于无向图(所有街道都是双行道)

欧拉回路条件

  • 必须每个路口(顶点)连接的街道数(即度数)都是偶数,并且整个城市(图)是连通的(你能从任何一个路口走到任何一个其他路口)

欧拉路径条件

  • 必须有且只有两个路口(顶点)连接的街道数(度数)是奇数,其余路口(顶点)连接的街道数(度数)都是偶数,并且整个城市(图)是连通的

通俗解释

  • 欧拉回路 就像你能从家出发,按顺序走过每条街道,并回到家。
  • 欧拉路径 就像你能从一个朋友家出发,按顺序走过每条街道,最后到自己家。

对于有向图(街道是单行道)

欧拉回路条件

  • 每个路口(顶点)的进来的街道数(入度)等于出去的街道数(出度),并且所有路口(顶点)都在一个能连通的范围内

欧拉路径条件

  • 必须有一个路口(顶点)只能进来但不能出去(入度比出度多1,终点)
  • 必须有一个路口(顶点)只能出去但不能进来(出度比入度多1,起点)
  • 其他所有路口(顶点)进来的街道数(入度)等于出去的街道数(出度)
  • 所有路口(顶点)都在一个能连通的范围内

通俗解释

  • 欧拉回路 就像你从一个路口出发,按顺序走过每条街道,最后又回到这个路口。
  • 欧拉路径 就像从一个指定的路口出发,按顺序走过每条街道,最后到达另一个特定的路口。

三、示例

无向图

存在欧拉回路的无向图

假设有一个三角形城市,三个路口(A、B、C),三条街道分别连接A-B、B-C、C-A。每个路口的度数都是2。你可以从任意一个路口出发,按顺序经过每条街道,最后回到起点。

在这里插入图片描述

存在欧拉路径但不存在欧拉回路的无向图

假设有一个线性城市,有5个路口(A、B、C、D、E),这5个路口通过4条街道连接形成一条线:

  • A-B
  • B-C
  • C-D
  • D-E

你可以从A开始走,经过B、C、D,最后到E,刚好走过每条街道一次,但你不能回到A。
在这里插入图片描述

有向图的例子

存在欧拉回路的有向图

假设有一个方形城市,4个路口(A、B、C、D),每条街道都是单行道,连接顺序是A到B,B到C,C到D,D到A。每个路口都有一条单行道进来,也有一条单行道出去。你可以从任意一个路口出发,按顺序经过每条街道,最后回到起点。
在这里插入图片描述

存在欧拉路径但不存在欧拉回路的有向图

假设有一个链形的城市,三个路口(A、B、C),两条街道分别是从A到B,从B到C。你可以从A出发,沿着单行道经过B,最后到达C。因为从C并没有回到A的单行道,所以不能形成回路。
在这里插入图片描述

四、找出欧拉路径/回路

Hierholzer算法

Hierholzer算法是一种用于寻找欧拉回路(或欧拉路径)的图算法。该算法由德国数学家Carl Hierholzer提出,具有高效的解决途径。它特别适用于连通图(或包含两个奇数顶点的连通图)。

基本原理

Hierholzer算法的基本思想是通过局部扩展的方法一步步构建出整个欧拉回路或欧拉路径。它的主要步骤包括:

  1. 从一个顶点开始,进行深度优先搜索(DFS),沿着未访问的边进行遍历,直到回到起点(形成一个部分回路或路径)。
  2. 如果当前路径未覆盖所有的边,则在已经形成的路径中寻找还存在未访问的边的顶点,再次进行深度优先搜索,生成更多的回路或路径。
  3. 将新形成的回路或路径直接插入到主路径中,不断扩展,直到遍历完所有边图。

实现步骤

  1. 判断图是否连通

    • 对于欧拉回路,所有顶点的度数均为偶数。
    • 对于欧拉路径,恰好有两个顶点的度数为奇数,其余顶点的度数为偶数。
  2. 选择起点

    • 对于欧拉回路,可从任意一个顶点开始。
    • 对于欧拉路径,从任意一个奇数度顶点开始(如果存在)。
  3. 构建路径

    • 从起点开始,沿未访问的边进行遍历,构建一条回路或路径。
    • 如果路径闭合形成回路,将该回路插入到主路径中。
    • 如果路径不闭合,继续寻找未访问边的顶点进行遍历直至无边可访问。
  4. 最终路径

    • 将所有回路或路径整合成一条完整的欧拉回路或欧拉路径,输出结果。

代码实现

# 1. 从一个顶点开始,沿着尚未使用的边遍历,一直走到不能继续为止(这个路径形成一个“回路”或部分“路径”)。
# 2. 如果路径的起点和终点是一样的,它就是一个回路,将这个回路插入到主路径中。
# 3. 如果路径的起点和终点不同,它就是一个“非闭合路径”,记录这个路径。
# 4. 继续从主路径中尚有未使用边的顶点开始重复上述过程,直到没有未使用的边为止。


class Graph:
    def __init__(self, edges):
        """初始化图类,构建邻接表"""
        self.graph = {}
        # 构建邻接表
        for frm, to in edges:
            if frm not in self.graph:
                self.graph[frm] = []
            if to not in self.graph:
                self.graph[to] = []
            self.graph[frm].append(to)
            self.graph[to].append(frm)

    def _remove_edge(self, frm, to):
        """从图中移除一条边"""
        self.graph[frm].remove(to)
        self.graph[to].remove(frm)

    def find_eulerian_path_or_circuit(self):
        """查找欧拉路径或欧拉回路"""

        # 找出所有度数为奇数的顶点
        odd_vertices = [v for v in self.graph if len(self.graph[v]) % 2 != 0]

        # 判定是否存在欧拉路径或欧拉回路
        # 欧拉路径:恰有两个奇数度顶点
        # 欧拉回路:所有顶点度数均为偶数
        if len(odd_vertices) not in [0, 2]:
            return "无欧拉路径或回路", []

        # 确定起始顶点
        # 若存在奇数度顶点,则从其中一个开始;否则从任意顶点开始
        start_vertex = odd_vertices[0] if len(odd_vertices) == 2 else next(iter(self.graph))

        eulerian_path = []  # 用于存储最终的欧拉路径或回路
        stack = [start_vertex]  # 栈,用于DFS遍历图

        while stack:
            vertex = stack[-1]  # 查看栈顶元素,但不弹出
            if self.graph[vertex]:  # 若当前顶点还有关联的边
                next_vertex = self.graph[vertex][-1]  # 找到下一条边连接的顶点
                stack.append(next_vertex)  # 将该顶点入栈
                self._remove_edge(vertex, next_vertex)  # 移除已遍历的边
            else:
                eulerian_path.append(stack.pop())  # 若当前顶点没有边,则从栈中弹出并记录路线

        eulerian_path.reverse()  # 由于路径是从终点往前记录的,因此需要反转路径顺序

        # 判断是欧拉路径还是欧拉回路
        if len(odd_vertices) == 0:
            result_type = "欧拉回路"
        else:
            result_type = "欧拉路径"

        return result_type, eulerian_path


if __name__ == '__main__':
    edges = [(0, 1), (0, 2), (1, 2), (2, 3), (1, 3)]
    graph = Graph(edges)
    result_type, eulerian_path = graph.find_eulerian_path_or_circuit()
    print(result_type, eulerian_path)

"""
输出:
欧拉路径 [1, 3, 2, 1, 0, 2]
"""

优点

  1. 高效性

    • 时间复杂度:Hierholzer算法的时间复杂度是O(E),其中E是图中的边数。因为每条边仅被访问和移除一次,算法非常高效,适用于大规模图。
  2. 实现简单

    • 该算法相对简单,核心的思路是递归或迭代地寻找回路并将其插入主回路。代码实现较为易懂,不涉及复杂的数据结构。
  3. 适用范围广

    • 该算法适用于无向图和有向图,只要图满足欧拉回路或欧拉路径的条件。
    • 具体判定条件:
      • 欧拉回路:无向图中所有顶点的度数均为偶数;有向图中所有顶点的入度等于出度。
      • 欧拉路径:无向图中恰有两个顶点的度数为奇数;有向图中恰有一个顶点的出度比入度大1,另一个顶点的入度比出度大1,其他顶点的入度等于出度。
  4. 局部最优构建全局最优

    • 通过局部回路组合成全局路径,逐步扩展,容易理解和实现。

缺点

  1. 前提条件限制

    • Hierholzer算法要求图必须满足相关欧拉回路或路径的条件,否则算法无法工作。因此需要预先进行连通性和度数检查。
  2. 空间复杂度

    • 需要额外的空间来存储邻接表和递归调用栈(或使用栈/队列进行迭代处理),虽然一般情况下空间复杂度是O(V + E),其中E是图中的边数V是图中的顶点数,但对于非常大的图来说,可能占用较多内存。
  3. 动态边处理

    • 虽然Hierholzer算法本身在处理每条边时是O(1)的,但在某些实现中,需要额外结构(例如链表或堆)来高效存取和删除边,可能增加实现复杂度。

五、结论

欧拉路径和欧拉回路作为图论中的经典问题,帮助我们分析图的性质。在实际应用中,比如进行网络设计、路线规划等,这些理论知识是非常有用的。

  • 15
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我爱让机器学习

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值