在以前的章节中,你已经看过深度优先和广度优先的搜索算法。这些算法形成了生成树。
生成树是无定向图的一个子图,包含图的所有顶点,用最少的边连接。生成树不能包含一个周期,也不能断开连接。
这里有一个图G和它所有可能的生成树。
从这个形成三角形的无向图中,你可以生成三种不同的生成树,其中你只需要两条边来连接所有顶点。
本章将介绍Prim算法,这是一种用于构建最小生成树的贪婪算法。贪婪算法是一步一步地构建解决方案,并在每一步中孤立地选择最优化的路径。
最小生成树可以使被选择来跨越该树的边的总重量最小化。它在各种情况下都有帮助。例如,你可能想找到最便宜的方式来布局水管网络。
下面是一个加权无向图的最小生成树的例子。
注意,只有第三个子图形成了最小生成树,因为它的总成本是3。
Prim的算法通过一次选择一条边来创建最小生成树。它是贪婪的,因为每次选择一条边,都是选择连接一对顶点的最小的加权边。
用Prim的算法寻找最小生成树有六个步骤。
想象一下,下面的图代表一个机场网络。顶点是机场,边表示飞机从一个机场飞到另一个机场的燃料成本。
让我们开始通过这个例子进行工作。
在图中选择任何一个顶点。让我们假设你选择了顶点2。
这个顶点的边的权重为[6, 5, 3]。一个贪婪的算法会选择权重最小的边。
选择权重为3的边,并与顶点5相连。
所探索的顶点是{2,5}。
从已探索的顶点中选择下一条最短的边。这些边是[6,5,6,6]。你选择权重为5的边,它与顶点3相连。
注意,顶点5和顶点3之间的边可以从考虑中删除,因为它已经是生成树的一部分。
探索的顶点是{2,3,5}。
接下来的潜在边是[6, 1, 5, 4, 6]。你选择权重为1的边,它与顶点1相连。
顶点2和顶点1之间的边可以被移除。
探索的顶点是{2, 3, 5, 1}。
从已探索的顶点中选择下一条最短的边。这些边是[5, 5, 4, 6]。你选择权重为4的边,它与顶点6相连。
顶点5和顶点6之间的边可以被删除。
所探索的顶点是{2, 5, 3, 1, 6}。
从已探索的顶点中选择下一条最短的边。这些边是[5,5,2]。你选择权重为2的边,它与顶点4相连。
从顶点1和顶点3连接到顶点4的边[5,5]可以被删除。
注意:如果所有的边都有相同的权重,你可以选择其中的任何一条。
这张最后的图是我们的例子中由Prim算法产生的最小生成树。
接下来,让我们看看如何在代码中建立这个。
打开本章的起始游戏场。这个操场带有一个邻接列表图和一个优先级队列,你将用它来实现Prim的算法。
优先级队列用于存储探索过的顶点的边。它是一个最小优先级队列,所以每次当你取消排队时,它会给你权重最小的那条边。
首先定义一个Prim类。打开Prim.swift,添加以下内容。
public class Prim<T: Hashable> {
public typealias Graph = AdjacencyList<T>
public init() {}
}
Graph被定义为AdjacencyList的一个类型别名。在未来,如果需要的话,你可以用一个邻接矩阵来代替它。
在构建算法之前,你将创建一些辅助方法,以保持你的组织性并合并重复的代码。
要创建一个最小生成树,你必须包括原图的所有顶点。打开AdjacencyList.swift,在AdjacencyList类中添加以下内容。
这就把一个图的所有顶点复制到一个新的图中。
除了复制图形的顶点,你还需要找到并存储你探索的每个顶点的边。打开Prim.swift,在Prim类中添加以下内容。
这个方法接收了四个参数。
当前的顶点。
图,其中存储了当前的顶点。
已经访问过的顶点。
增加所有潜在边的优先队列。
在这个函数中,你要做以下工作。
查看与当前顶点相邻的每一条边。
检查目标顶点是否已经被访问过。
如果它没有被访问过,你就把这条边添加到优先级队列中。
现在你已经建立了辅助方法,你可以实现Prim的算法。
在类Prim中添加以下方法。
下面是你目前拥有的东西。
produceMinimumSpanningTree接收一个无向图,并返回一个最小生成树和它的成本。
cost跟踪最小生成树中各条边的总权重。
这是一个图,将成为你的最小生成树。
visited存储所有已经被访问过的顶点。
这是一个最小优先级的队列,用来存储边。
接下来,继续用下面的方法实现 produceMinimumSpanningTree。
这段代码启动了该算法。
将原图中的所有顶点复制到最小生成树中。
从图中获取起始顶点。
将起始顶点标记为已访问。
将所有从起始顶点开始的潜在边添加到优先级队列中。
最后,用以下方法完成produceMinimumSpanningTree。
翻阅代码。
继续Prim的算法,直到边的队列为空。
获取目标顶点。
如果这个顶点已经被访问过了,重新开始循环,得到下一个最小的边。
将目标顶点标记为已访问。
把边的权重加到总成本中。
将最小的边加入你正在构建的最小生成树中。
添加当前顶点的可用边。
一旦优先级队列为空,返回最小成本和最小生成树。
导航到主操场,你会看到上面的图已经用邻接列表构造好了。
现在是时候看看Prim的算法的作用了。添加以下代码。
let (cost,mst) = Prim().produceMinimumSpanningTree(for: graph)
print("cost: \(cost)")
print("mst:")
print(mst)
这段代码从例子部分构造了一个图。你会看到以下输出。
cost: 15.0
mst:
5 ---> [ 2 ]
6 ---> [ 3, 4 ]
3 ---> [ 2, 1, 6 ]
1 ---> [ 3 ]
2 ---> [ 5, 3 ]
4 ---> [ 6 ]
在上面的算法中,你维护了三个数据结构。
建立最小生成树的邻接列表图。将顶点和边添加到邻接列表中是O(1)。
一个集合,用于存储所有你访问过的顶点。向集合添加顶点和检查集合是否包含一个顶点的时间复杂度也是O(1)。
一个最小优先级队列,当你探索更多的顶点时存储边。优先级队列建立在一个堆上,插入需要O(log E)。
Prim算法的最坏情况下的时间复杂性是O(E log E)。每次你从优先级队列中提取最小的边时,你必须遍历目的地顶点的所有边( O(E)),并将该边插入优先级队列( O(logE))。
生成树是无向图的一个子图,包含所有边数最少的顶点。
Prim算法是一种构建最小生成树的贪婪算法,它在算法的每一步都使每条边的权重最小。
为了实现Prim的算法,你可以利用三种不同的数据结构:优先级队列、集合和邻接列表。
上一章 | 目录 | 下一章 |
---|