本关任务:编写代码完成 Prim 算法的 Python 实现,求出图的最小生成树。
为了完成本关任务,你需要掌握: 1.最小生成树的基本概念; 2.如何实现 Prim 算法。
最小生成树
为了说明图的最小生成树算法,首先我们来考虑一个在互联网中网游设计者和网络收音机所面临的问题:信息广播问题。网游需要让所有玩家获知其他玩家所在的位置,收音机则需要让所有听众获取直播的音频数据。图 1 展示了这个广播问题,最上方是一个广播的服务器,其他位置有 4 台接收的收听设备,通过路由器网络可以将广播数据发送出去,从而实现将相同的数据发送到不同的结点。
图1 信息广播问题
为了解决信息广播问题,有以下几种方法:
信息广播问题最简单的解法是由广播源维护一个收听者的列表,将每条消息向每个收听者发送一次。由于图 1 中有 4 个收听者,每条消息将会被发送 4 次。在路由器网络中,每个消息都采用最短路径算法到达收听者。
但是这种方法会导致某些路由器重复发送相同消息,比如路由器 A 会处理 4 次相同消息,而 B、D 位于其它 3 个收听者的最短路径上,则各会处理转发 3 次相同消息。这样就会给该路由器网络增加负担,从而产生许多额外流量。
是一种信息广播问题的暴力解法,该方法将每条消息在路由器间散布出去,所有路由器都将收到的消息转发到自己相邻的路由器和收听者。显然,如果没有任何限制,这个方法将造成网络洪水灾难,很多路由器和收听者会不断重复收到相同的消息,永不停止。
所以洪水解法一般会给每条消息附加一个生命值(TTL:Time To Live),初始设置为从消息源到最远收听者的距离。每当一个路由器收到一条消息,如果其 TTL 值大于 0,则将 TTL 减少 1 后再转发出去;如果 TTL 等于 0 了,则直接抛弃这个消息。TTL 的设置防止了灾难发生,但这种洪水解法显然比前述的单播方法所产生的流量还要大。
是信息广播问题的最优解法,该方法依赖于在路由器关系图上选取具有最小权重的生成树。其中生成树表示为拥有图中所有的顶点和最少数量的边,以保持连通的子图。也就是说,从生成树中移去一条边,这个树就无法连通。
对于图 G(V,E),其最小生成树 T 定义为包含所有顶点 V,以及边 E 的无圈子集,并且边上权重之和最小。
图 2 展示了为了解决图 1 中的信息广播问题而形成的最小生成树。每一个路由器向作为生成树一部分的任意相邻路由器发送信息,这样信息广播就只需要从 A 开始,沿着树的路径层次向下传播。在此问题中,A 向 B 发送信息,B 向 D 和 C 发送信息,D 向 E 发送信息,E 向 F 发送信息,F 向 G 发送信息。这样每个路由器只需要处理 1 次消息就能让所有收听者都能够接收到,同时总费用最小。
图2 广播问题的最小生成树
Prim 算法
以上介绍了最小生成树在信息广播中的应用,最小生成树问题可以使用 Prim 算法来解决。Prim 算法属于“贪心算法”,即每步都沿着最小权重的边向前搜索。Prim 算法构造最小生成树的思路很简单,如果 T 还不是生成树,则反复做:
找到一条最小权重的可以安全添加的边;
将边添加到树 T。
其中“可以安全添加的边”定义为一端顶点在树中另一端不在树中的边,以便保持树的无圈特性。
与 Dijkstra 算法的实现类似,我们在顶点 Vertex 类中添加了 dist 和 pred 实例变量,dist 用来表示该顶点到已并入当前生成树的顶点的最短距离边上的权值,pred 用来表示该最短距离边另一端在树中的顶点。Vertex 类中对这两个实例变量的定义和相关方法的代码示例如下:
class Vertex:
def __init__(self,num):
……
self.dist = sys.maxsize # 距离 dist
self.pred = None # 前驱
def setDistance(self,d): # 设置 dist
self.dist = d
def setPred(self,p): # 设置前驱
self.pred = p
def getDistance(self): # 获取 dist
return self.dist
def getPred(self): # 获取前驱
return self.pred
……
同时还需要使用一个优先队列来控制顶点的优先级,其中决定顺序的参量是顶点的 dist 值。我们采用二叉堆的数据结构来实现优先队列,相关知识请参考二叉堆实现的优先队列。与此不同的是,为了对优先队列进行重排序,在 PriorityQueue 类中还添加了decreaseKey
方法,当队列中某个顶点的 dist 减小的时候,将会对这个顶点进行调整,在堆中上浮到最终正确的位置。下面为一些相关的基本操作定义:
以下为 Prim 算法求解最小生成树的一个简单示例。
对于图 3 所示的图,我们将顶点 A 作为开始顶点。最初,只将开始顶点 A 的 dist 设置为 0,而其他所有顶点的 dist 都设置为最大整数 sys.maxsize,并且全部加入到优先队列中,在图中顶点的 dist 用 d 表示。首先,从优先队列中将顶点 A 出队,将顶点 A 作为当前生成树。从 A 到顶点 B、C 的距离 2 和 3 都小于这两个顶点的 dist 初始值 sys.maxsize,于是更新顶点 B、C 的 dist 分别为 2 和 3。这时 B 和 C 已到了优先队列的前端,Prim 算法结果和优先队列 PQ 的状态如图 3 所示。
图3 顶点 A 出队,更新各顶点 dist
接下来需要寻找一条最小权重的可以安全添加的边,即找到一个与当前生成树中顶点距离最近的顶点。由于顶点 B 的 dist 最小,位于优先队列的最前端,于是将 B 出队,选择从 A 到 B 的这条边,将 B 加入到当前生成树。接下来直接对顶点 B 到未访问相邻顶点 C、D、E 的距离与顶点的 dist 进行比较,若距离更小,则将顶点的 dist 更新为较小值。将 D 的 dist 更新为从 B 到 D 的距离 1,C 的 dist 更新为从 B 到 C 的距离 1,E 的 dist 更新为从 B 到 E 的距离 4。根据顶点新的 dist 值对优先队列重新排序,这一步的算法结果和优先队列 PQ 的状态如图 4 所示。
图4 选择顶点 A、B 间的边
此时位于优先队列前端的是顶点 C,于是将 C 出队,选择从 B 到 C 的这条边,将 C 加入到当前生成树。接下来对顶点 C 到未访问相邻顶点 F 的距离与 F 的 dist 进行比较,距离 5 小于 F 的初始 dist,直接将 F 的 dist 更新为从 C 到 F 的距离 5。根据顶点新的 dist 值对优先队列重新排序,这一步的算法结果和优先队列 PQ 的状态如图 5 所示。
图5 选择顶点 B、C 间的边
此时位于优先队列前端的是顶点 D,于是将 D 出队,选择从 B 到 D 的这条边,将 D 加入到当前生成树。由于从 D 到 E 的距离 1 小于 E 的 dist 值 4,则将 E 的 dist 更新为 1。根据顶点新的 dist 值对优先队列重新排序,这一步的算法结果和优先队列 PQ 的状态如图 6 所示。
图6 选择顶点 B、D 间的边
此时位于优先队列前端的是顶点 E,于是将 E 出队,选择从 D 到 E 的这条边,将 E 加入到当前生成树。由于从 E 到 F 的距离 1 小于 F 的 dist 值 5,则将 F 的 dist 更新为 1。根据顶点新的 dist 值对优先队列重新排序,这一步的算法结果和优先队列 PQ 的状态如图 7 所示。
图7 选择顶点 B、E 间的边
此时位于优先队列前端的是顶点 F,于是将 F 出队,选择从 E 到 F 的这条边,将 F 加入到当前生成树。由于从 F 到 G 的距离 1 小于 G 的 dist 值,则将 G 的 dist 更新为 1。根据顶点新的 dist 值对优先队列重新排序,这一步的算法结果和优先队列 PQ 的状态如图 8 所示。
图8 选择顶点 E、F 间的边
此时位于优先队列的只有顶点 G,于是将 G 出队,选择从 F 到 G 的这条边,将 G 加入到当前生成树。由于队列已空,Prim 算法结束,求出的最小生成树如图 9 所示,其中所有边的权值总和最小,同时也可以发现所有顶点的 dist 值总和即为所求最小生成树边上权值总和。
图9 选择顶点 F、G 间的边
在右侧编辑器中的 Begin-End 区间补充代码,根据 Prim 算法的算法思想和所展示出的优先队列 PriorityQueue 类,完成prim
方法,求出无向赋权图的最小生成树以及边上的权值总和。
平台会对你编写的代码进行测试,比对你输出的数值与实际正确的数值,只有所有数据全部计算正确才能通过测试:
测试输入:
7,0 1 2,0 2 3,1 2 1,1 3 1,3 4 1,1 4 4,2 5 5,4 5 1,5 6 1
输入说明:输入字符串第一个逗号前的数值表示所创建的无向图的顶点数,剩下的部分同样以逗号进行分隔。分隔成的每一小段又以空格分隔成三部分,前两部分表示所添加边两端的顶点,最后一部分表示边上的权值。
预期输出:
7
输出说明:输出为对所创建图以 0 为开始顶点,通过 Prim 算法求出的最小生成树边上的权值总和。
测试输入:
6,0 1 5,0 5 2,1 2 4,2 3 9,3 4 7,3 5 3,5 4 8,5 2 1
预期输出:
17
提示:
from pythonds.graphs import PriorityQueue,Graph
g = Graph()
g.addVertex(0)
g.addVertex(1)
g.addEdge(0, 1, 5)
pq = PriorityQueue()
pq.buildHeap([(v.getDistance(), v) for v in g])
g.addVertex(2)
g.addEdge(0, 2, 6)
for v2 in g:
if v2 not in pq:
cost = g.vertices[0].getWeight(v2)
print(cost)
提示说明:根据该提示可知道如何判定某顶点是否在优先队列中、如何获取两顶点间边上的权值,Graph 和 Vertex 类的相关知识请参考图抽象数据类型的 Pyth的 Python 实现。
输出:
6
import sys
'''请在Begin-End之间补充代码, 完成Prim函数'''
class PriorityQueue:
def __init__(self):
self.heapArray = [(0,0)] # 初始化一个列表,用来保存堆数据
self.currentSize = 0 # 用来跟踪记录堆当前的大小
# 从无序表建立一个堆
def buildHeap(self,alist):
self.currentSize = len(alist)
self.heapArray = [(0,0)]
for i in alist:
self.heapArray.append(i)
i = len(alist) // 2
while (i > 0):
self.percDown(i)
i = i - 1
def percDown(self,i):
while (i * 2) <= self.currentSize:
mc = self.minChild(i)
if self.heapArray[i][0] > self.heapArray[mc][0]:
tmp = self.heapArray[i]
self.heapArray[i] = self.heapArray[mc]
self.heapArray[mc] = tmp
i = mc
# 求出最小子结点
def minChild(self,i):
if i*2 > self.currentSize:
return -1
else:
if i*2 + 1 > self.currentSize:
return i*2
else:
if self.heapArray[i*2][0] < self.heapArray[i*2+1][0]:
return i*2
else:
return i*2+1
# 不断交换,直到新结点“上浮”到正确位的置来保持堆次序
def percUp(self,i):
while i // 2 > 0:
if self.heapArray[i][0] < self.heapArray[i//2][0]:
tmp = self.heapArray[i//2]
self.heapArray[i//2] = self.heapArray[i]
self.heapArray[i] = tmp
i = i//2
# 添加新的的数据
def add(self,k):
self.heapArray.append(k)
self.currentSize = self.currentSize + 1
self.percUp(self.currentSize)
# 移走堆中的最小项
def delMin(self):
retval = self.heapArray[1][1]
self.heapArray[1] = self.heapArray[self.currentSize]
self.currentSize = self.currentSize - 1
self.heapArray.pop()
self.percDown(1)
return retval
# 返回堆是否为空
def isEmpty(self):
if self.currentSize == 0:
return True
else:
return False
# 结点 val 的 key 改变为 amt,并对堆进行重新调整
def decreaseKey(self,val,amt):
done = False
i = 1
myKey = 0
while not done and i <= self.currentSize: # 找到顶点val
if self.heapArray[i][1] == val:
done = True
myKey = i
else:
i = i + 1
if myKey > 0:
self.heapArray[myKey] = (amt,self.heapArray[myKey][1])
self.percUp(myKey)
def __contains__(self,vtx):
for pair in self.heapArray:
if pair[1] == vtx:
return True
return False
def prim(G,start):
pq = PriorityQueue()
for v in G:
v.setDistance(sys.maxsize) # 所有顶点的dist都设置为最大值
v.setPred(None) # 设置所有的顶点的前驱
start.setDistance(0) # 设置开始顶点的dist为0
pq.buildHeap([(v.getDistance(), v) for v in G]) # 对所有顶点建堆,形成优先队列
while not pq.isEmpty(): # 当优先队列不为空时做以下操作
# 从优先队列中出队一个顶点作为currentVert
# ********** Begin ********** #
currentVert = pq.delMin()
# ********** End ********** #
for nextVert in currentVert.getConnections(): # 对与当前顶点相邻的所有顶点进行扫描
# 给newCost赋值为当前顶点与相邻顶点nextVert之间边上的权重
# 若该顶点在优先队列中(不在生成树中)并且与当前生成树的距离小于之前的dist,就进行更新
# 修改该顶点的前驱
# 修改该顶点的dist
# ********** Begin ********** #
newCost = currentVert.getWeight(nextVert)
if nextVert in pq and newCost < nextVert.getDistance():
nextVert.setPred(currentVert)
nextVert.setDistance(newCost)
# ********** End ********** #
pq.decreaseKey(nextVert,newCost) # 优先队列重排