专栏名称: 新机器视觉
最前沿的机器视觉与计算机视觉技术
目录
相关文章推荐
九章算法  ·  “小透明”码农,逆袭了 ·  3 天前  
每日意图  ·  6个10秒,6个人生重要时刻 ·  昨天  
每日意图  ·  6个10秒,6个人生重要时刻 ·  昨天  
九章算法  ·  湾区不卷娃的底气! ·  6 天前  
九章算法  ·  跳槽至少要涨多少钱? ·  4 天前  
51好读  ›  专栏  ›  新机器视觉

有趣的图形算法(Prim算法)--(34)

新机器视觉  · 公众号  · 算法  · 2025-01-06 11:11

主要观点总结

Prim算法是一种构建最小生成树的算法,它从完整的图中选择边成本最小的子集,确保所有节点连通。算法流程包括选定起始节点,逐步构建最小生成树,更新节点和边的信息,直到所有节点都被访问。代码实现涉及创建数据结构、优先级队列、未访问节点集合等。通过示例图解展示了Prim算法在图形上的运作过程。

关键观点总结

关键观点1: Prim算法核心

从完整的图中挑选边成本最小的子集,确保各个节点全部连通。

关键观点2: 算法流程

选定起始节点,逐步构建最小生成树,更新节点和边的信息,直到所有节点都被访问。

关键观点3: 代码实现

涉及创建数据结构、优先级队列、未访问节点集合等,通过自定义的PriorityQueue实现管理维护关键数据。

关键观点4: 算法应用示例

通过岛屿桥梁建设的例子,形象展示Prim算法的运行过程。


正文

 

Prim算法

构造最小生成树,就得依靠特定算法,其核心在于从完整的图里挑出边的成本最小的那个子集,如此一来,生成的图才能保证各个节点全部连通。查找图的最小生成树时,有个常用的方法叫 Prim 算法,这一算法可是由好些人各自独立琢磨出来的,其中就包括计算机科学家 R.C. Prim 以及数学家 Vojteˇch Jarník 。Prim 算法的运行流程,与 Dijkstra 算法颇有几分相似,都是围绕着未访问节点的集合展开运作,一步一步地逐个构建节点,进而形成最小生成树。

Prim 算法启动之初,面对的是一整套尚未被访问的所有节点,接着随机选定一个节点率先进行访问,而这个被率先访问的节点,就成了构建最小生成树的起始点。在后续的每一轮迭代当中,算法都会在那些还没被访问过的节点里头,找出边权重最小的那个节点,前提是这个节点的边权重必须小于之前已经访问过的任意节点的边权重。算法还会找出这类节点:“距离我们已有的节点集合边缘最近,能以最低成本添加入集合”。随后,算法就把这个新发现的节点从未访问节点集合里剔除出去,再把与之对应的那条边,添加到成本最低的生成树当中。就这样,算法持续不断地逐一添加节点与边,每一轮迭代完成一个操作,直至把所有节点统统访问完毕。

Prim 算法运行过程中,每个节点最多被访问一回,每条边最多被检测两次,也就是分别从边的两端各考虑一次。而且,针对每一个节点而言,当以标准heap来实现优先队列时,我们或许会看到有多达 |V| 次的节点插入或者更新操作。综合这些因素,该算法的总体成本大致为 (|V| + |E|)× log (|V |)。

不妨把 Prim 算法具象化地想象成这么一个场景:有一家建筑公司承接了一项工程,要对群岛之间的桥梁进行升级改造。这家公司打算把连接群岛的那些破旧腐朽的木桥,全都换成崭新的现代化桥梁。由于旧木桥根本承载不了施工设备的重量,站在建筑公司的角度来看,只有靠新建的桥梁连接起来的岛屿,才算是真正意义上实现了连通。并且,合同里明确规定,到最后,任意两座岛屿之间,都得能够通过一座新建的现代化桥梁顺利抵达。

施工人员率先从一座岛屿入手,接着逐步向外拓展,用新建的桥梁把越来越多的岛屿连接起来。在每一个施工步骤里,他们都会挑选出将当前已连接岛屿集合与集合外岛屿相连接的最短木桥,优先将其升级改造。通过始终以已连接集合中的岛屿作为新建桥梁的起点,施工人员就能借助现代化桥梁,把施工设备搬运到新开拓边缘的起始岛屿上。而始终把新建桥梁的终点落在连通集之外的孤岛上,施工人员就能在每个施工阶段,持续扩大连通集所覆盖的范围。

代码实现

在 Prim 算法具体推进的每一个环节当中,我们都得时刻留意那些尚未连接起来的节点,同时密切追踪连接它们时所能采用的最优边权重情况。为此,我们借助自定义的 PriorityQueue(优先队列)实现方式来管理、维护这些关键数据,这种自定义实现手段为我们提供了一套行之有效的机制,能够便捷地在队列里查找特定数值,还能灵活地对各项优先级进行调整修改。就当前这段涉及的代码而言,大家只需掌握几个基础操作就够了:将元素项目插入到优先级队列里,以及从优先级队列当中移除项目,还有就是修改元素的优先级。

再聚焦到代码本身的运行逻辑,它会开启一个循环流程,持续不断地遍历访问优先级队列里的节点,直至整个队列变为空集。每当从优先级队列(也就是那些尚未访问过的节点集合)里移除一个新节点的时候,代码就会立即着手核查这个节点周边那些还未被访问过的相邻节点,并且细致审视当前节点是否能够为它的任意一个尚未连接的相邻节点,提供一条更为理想的边,这里所说的 “更为理想”,指的就是边的成本更低。一旦判定情况属实,代码就会即刻启用新发现的边以及对应的权重数值,对相邻节点的相关信息予以更新:

def prims(g: Graph) -> Union[list, None]:
    pq: PriorityQueue = PriorityQueue(min_heap=True)
    last: list = [-1] * g.num_nodes
    mst_edges: list = []

    pq.enqueue(0, 0.0)
    for i in range(1, g.num_nodes):
        pq.enqueue(i, float('inf'))

    while not pq.is_empty():
        index: int = pq.dequeue()
        current: Node = g.nodes[index]

        if last[index] != -1:
            mst_edges.append(current.get_edge(last[index]))
        elif index != 0:
            return None

        for edge in current.get_edge_list():
            neighbor: int = edge.to_node
            if pq.in_queue(neighbor):

                if edge.weight < pq.get_priority(neighbor):
                    pq.update_priority(neighbor, edge.weight)
                    last[neighbor] = index

    return mst_edges

这段代码的执行流程如下:首先,它会构建三个辅助性的数据结构。其一为基于最小堆的未连接节点优先级队列,用 “pq” 表示;其二是一个数组,用于记录在给定节点之前所访问的最后一个节点;其三是最小生成树的最终边集,记为 “mst_edges”。在代码运行时,需要引入自定义的 “PriorityQueue” 类,同时还要从 Python 的 “typing” 库中导入 “Union”。

算法启动之初,所有节点都会被插入到优先级队列里。其中,起始节点 “0” 的优先级设定为 0.0,而其余节点的优先级均设为无穷大。随后,代码的执行过程与迪杰斯特拉算法类似,每次处理一个尚未访问的节点。“while” 循环会持续迭代,直至未访问节点的优先级队列为空。每一次迭代时,都会挑选出与已访问节点距离最近的节点,并将其从优先级队列中移除,如此一来,也就相当于把该节点从未访问节点集合里剔除了。

接下来,代码会核查是否存在指向连通集中某个节点的边。有两种情形下,节点的 “last” 条目可能为 -1 :第一种情况是节点 0,因其率先被探索,所以不存在前置节点;第二种情况是处于从节点 0 无法访问的断开连接组件 “index” 中。要是遇到后一种情况,意味着无法连接所有节点,该图也就不存在最小生成树,此时函数会返回 “None”。

在新节点经出列操作被添加到已访问节点集合之后,“for” 循环会遍历此节点的每一个邻居节点,查看邻居节点是否尚未被访问(也就是仍在优先级队列里)。倘若如此,代码就会对比先前的最佳边权重与新边的权重,以此判断是否找到了更佳的节点边。最终,代码会返回构成最小生成树的边集。

需要留意的是,倘若图是断开连接的状态,那么每个连通分量都有其自身的最小生成树。针对此处所介绍代码,还有另一种处理方式,即返回为每个连通分量创建最小生成树的边列表。要达成这一目标,我们可以删掉 “elif check” 及其对应的返回值。此后,代码就能通过从优先级队列里选取一个节点,并持续挑选边的方式,转移至下一个组件继续处理。

举个栗子

下图呈现了 Prim 算法在包含 8 个节点的图形上的运作图示。在每个子图的右侧,都附有一张表格,用于记录为每个节点所追踪的各类信息,涵盖节点的 ID、存储已连接节点集到该节点距离的节点优先级,以及列表中所存储的当前已连接子集中距离最近成员的 “last” 值。除首个节点外,其余所有节点初始状态下的距离均设为无穷大,“last” 节点指针设为 - 1,这表明此时尚未探寻到通往这些节点的路径。当某个节点从优先级队列中被移除后,我们会将其所在行以灰色显示,以此表明后续不再对该节点予以考虑。

搜索流程起始于图 10 - 3(a)中的节点 0,这就如同我们假设的岛屿桥梁建设公司在其本土岛屿的总部开启业务运作一般。搜索操作会先将此节点从优先级队列中移除,接着核查节点 0 的每一个相邻节点,并依据核查结果相应地更新信息。经此操作,节点 1 与节点 0 的距离确定为 1.0,节点 3 与节点 0 的距离为 0.6。此刻,这两个相邻节点的 “last” 值均指向节点 0,意味着在当前连接子集中,节点 0 是它们距离最近的节点。

在图 10 - 3(b)阶段,搜索将会朝着不在已连接子集中且距离最近的节点推进,这一过程类似在岛屿之间搭建第一座桥梁。此时,算法会将距离(即优先级)为 0.6 的节点 3 从队列中移出,使其加入已连接子集,随后检查节点 3 的相邻节点 4 和 6,因为通过节点 3 新增的边,这两个节点此刻能够被触及到。搜索过程会据此更新节点 4 和节点 6 的优先级以及 “last” 值。

紧接着,搜索进程会对图 10 - 3(c)中的节点 1 展开探索。在检查节点 1 的相邻节点时,发现存在一条更短的边连接至节点 4。这好比工人察觉到旧的木桥(1,4)相较于当前计划升级的另一座木桥(3,4),长度更短,意味着升级所需成本更低。基于此发现,搜索操作会将节点 4 的距离更新为 0.5,并同步更新指向 “last” 节点 1 的指针,以此反映出连接边的起始节点。至此,搜索计划变更为采用边(1,4)将节点 4 接入我们的连接集,而非先前规划的边(3,4)。

图 10-3: Prim算法图解

在后续的五个子图当中,搜索进程会分别推进至节点 5、节点 2、节点 4、节点 6 以及节点 7。在此过程中,需对每个节点的未访问相邻节点展开检查,一旦发现存在可形成更短边的相邻节点,便即时对其进行更新操作。如此一来,连通子图的规模将会逐次递增,每次增加一个节点,持续这一操作直至所有节点均实现连通状态。

版权声明


  转自阿雁闲聊,版权属于原作者,仅用于学术分享