假设我们要把所有15个最大的MSA都连入超级高铁网络,目标是要最大限度地降低网络的铺设成本,于是就意味着所用的轨道数量要最少。于是问题就成了:如何用最少的轨道连接所有MSA?

4.4.1 权重的处理

要了解建造某条边所需的轨道数量,就需要知道这条边表示的距离。现在是再次引入权重概念的时候了。在超级高铁网络中,边的权重是两个所连MSA之间的距离。图4-5与图4-2几乎相同,差别只是每条边多了权重,表示边所连的两个顶点之间的距离(以英里为单位)。

图4-5 美国15个最大的MSA的加权图,权重代表两个MSA之间的距离,单位英里(1英里≈1.6093 km)

为了处理权重,需要建立Edge的子类WeightedEdge和Graph的子类WeightedGraph。每个WeightedEdge都带有一个与其关联的表示其权重的float类型数据。下面马上就会介绍Jarník算法,它能够比较两条边并确定哪条边权重较低。采用数值型的权重就很容易进行比较了。具体代码如代码清单4-6所示。

代码清单4-6 weighted_edge.py

from __future__ import annotations
from dataclasses import dataclassfrom edge import Edge@dataclassclass WeightedEdge(Edge):    weight: float    def reversed(self) -> WeightedEdge:return WeightedEdge(self.v, self.u, self.weight)# so that we can order edges by weight to find the minimum weight edge    def __lt__(self, other: WeightedEdge) -> bool:return self.weight<other.weightdef __str__(self) -> str:return f"{self.u} {self.weight}> {self.v}"

WeightedEdge的实现代码与Edge的实现代码并没有太大的区别,只是添加了一个weight属性,并通过__lt__()实现了“<”操作符,这样两个WeightedEdge就可以相互比较了。“<”操作符只涉及权重(而不涉及继承而来的属性u和v),因为Jarník的算法只关注如何找到权重最小的边。

如代码清单4-7所示,WeightedGraph从Graph继承了大部分功能,此外,它还包含了__init__方法和添加WeightedEdge的便捷方法,并且实现了自己的__str__()方法。它还有一个新方法neighbors_for_index_with_weights(),这一方法不仅会返回每一位邻居,还会返回到达这位邻居的边的权重。这一方法对其__str__()十分有用。

代码清单4-7 weighted_graph.py

from typing import TypeVar, Generic, List, Tuple
from graph import Graphfrom weighted_edge import WeightedEdgeV = TypeVar('V') # type of the vertices in the graph
class WeightedGraph(Generic[V], Graph[V]):    def __init__(self, vertices: List[V] = []) -> None:self._vertices: List[V] = verticesself._edges: List[List[WeightedEdge]] = [[] for _ in vertices]def add_edge_by_indices(self, u: int, v: int, weight: float) -> None:edge: WeightedEdge = WeightedEdge(u, v, weight)        self.add_edge(edge) # call superclass versiondef add_edge_by_vertices(self, first: V, second: V, weight: float) -> None:u: int = self._vertices.index(first)v: int = self._vertices.index(second)self.add_edge_by_indices(u, v, weight)def neighbors_for_index_with_weights(self, index: int) -> List[Tuple[V, float]]:distance_tuples: List[Tuple[V, float]] = []        for edge in self.edges_for_index(index):distance_tuples.append((self.vertex_at(edge.v), edge.weight))return distance_tuplesdef __str__(self) -> str:desc: str = ""for i in range(self.vertex_count):desc += f"{self.vertex_at(i)} -> {self.neighbors_for_index_with_weights(i)}\n"return desc

现在可以实际定义加权图了。这里将会用到图4-5表示的加权图,名为city_graph2。具体代码如代码清单4-8所示。

代码清单4-8 weighted_graph.py(续)

if __name__ == "__main__":city_graph2: WeightedGraph[str] = WeightedGraph(["Seattle", "San Francisco", "Los Angeles", "Riverside", "Phoenix", "Chicago", "Boston", "New York", "Atlanta","Miami", "Dallas", "Houston", "Detroit", "Philadelphia", "Washington"])city_graph2.add_edge_by_vertices("Seattle", "Chicago", 1737)city_graph2.add_edge_by_vertices("Seattle", "San Francisco", 678)city_graph2.add_edge_by_vertices("San Francisco", "Riverside", 386)city_graph2.add_edge_by_vertices("San Francisco", "Los Angeles", 348)city_graph2.add_edge_by_vertices("Los Angeles", "Riverside", 50)city_graph2.add_edge_by_vertices("Los Angeles", "Phoenix", 357)city_graph2.add_edge_by_vertices("Riverside", "Phoenix", 307)city_graph2.add_edge_by_vertices("Riverside", "Chicago", 1704)city_graph2.add_edge_by_vertices("Phoenix", "Dallas", 887)city_graph2.add_edge_by_vertices("Phoenix", "Houston", 1015)city_graph2.add_edge_by_vertices("Dallas", "Chicago", 805)city_graph2.add_edge_by_vertices("Dallas", "Atlanta", 721)city_graph2.add_edge_by_vertices("Dallas", "Houston", 225)city_graph2.add_edge_by_vertices("Houston", "Atlanta", 702)city_graph2.add_edge_by_vertices("Houston", "Miami", 968)city_graph2.add_edge_by_vertices("Atlanta", "Chicago", 588)city_graph2.add_edge_by_vertices("Atlanta", "Washington", 543)city_graph2.add_edge_by_vertices("Atlanta", "Miami", 604)city_graph2.add_edge_by_vertices("Miami", "Washington", 923)city_graph2.add_edge_by_vertices("Chicago", "Detroit", 238)city_graph2.add_edge_by_vertices("Detroit", "Boston", 613)city_graph2.add_edge_by_vertices("Detroit", "Washington", 396)city_graph2.add_edge_by_vertices("Detroit", "New York", 482)city_graph2.add_edge_by_vertices("Boston", "New York", 190)city_graph2.add_edge_by_vertices("New York", "Philadelphia", 81)city_graph2.add_edge_by_vertices("Philadelphia", "Washington", 123)print(city_graph2)

因为WeightedGraph实现了__str__(),所以我们可以美观打印出city_graph2。在输出结果中会同时显示每个顶点连接的所有顶点及这些连接的权重。

Seattle -> [('Chicago', 1737), ('San Francisco', 678)]
San Francisco -> [('Seattle', 678), ('Riverside', 386), ('Los Angeles', 348)]
Los Angeles -> [('San Francisco', 348), ('Riverside', 50), ('Phoenix', 357)]
Riverside -> [('San Francisco', 386), ('Los Angeles', 50), ('Phoenix', 307), ('Chicago',1704)]
Phoenix -> [('Los Angeles', 357), ('Riverside', 307), ('Dallas', 887), ('Houston', 1015)]
Chicago -> [('Seattle', 1737), ('Riverside', 1704), ('Dallas', 805), ('Atlanta', 588), ('Detroit', 238)]
Boston -> [('Detroit', 613), ('New York', 190)]
New York -> [('Detroit', 482), ('Boston', 190), ('Philadelphia', 81)]
Atlanta -> [('Dallas', 721), ('Houston', 702), ('Chicago', 588), ('Washington', 543),('Miami', 604)]
Miami -> [('Houston', 968), ('Atlanta', 604), ('Washington', 923)]
Dallas -> [('Phoenix', 887), ('Chicago', 805), ('Atlanta', 721), ('Houston', 225)]
Houston -> [('Phoenix', 1015), ('Dallas', 225), ('Atlanta', 702), ('Miami', 968)]
Detroit -> [('Chicago', 238), ('Boston', 613), ('Washington', 396), ('New York', 482)]
Philadelphia -> [('New York', 81), ('Washington', 123)]
Washington -> [('Atlanta', 543), ('Miami', 923), ('Detroit', 396), ('Philadelphia', 123)]

4.4.2 查找最小生成树

树是一种特殊的图,它在任意两个顶点之间只存在一条路径,这意味着树中没有环路(cycle),有时被称为无环(acyclic)。环路可以被视作循环。如果可以从一个起始顶点开始遍历图,不会重复经过任何边,并返回到起始顶点,则称存在一条环路。任何不是树的图都可以通过修剪边而成为树。图4-6演示了通过修剪边把图转换为树的过程。

图4-6 在左图中,在顶点B、C和D之间存在一个环路,因此它不是树。在右图中,
连通C和D的边已被修剪掉了,因此它是一棵树

连通图(connected graph)是指从图的任一顶点都能以某种路径到达其他任何顶点的图。本章中的所有图都是连通图。生成树(spanning tree)是把图所有顶点都连接起来的树。最小生成树(minimum spanning tree)是以最小总权重把加权图的每个顶点都连接起来的树(相对于其他的生成树而言)。对于每张加权图,我们都能高效地找到其最小生成树。

这里出现了一大堆术语!“查找最小生成树”和“以权重最小的方式连接加权图中的所有顶点”的意思相同,这是关键点。对任何设计网络(交通网络、计算机网络等)的人来说,这都是一个重要而实际的问题:如何能以最低的成本连接网络中的所有节点呢?这里的成本可能是电线、轨道、道路或其他任何东西。以电话网络来说,这个问题的另一种提法就是:连通每个电话机所需要的最短电缆长度是多少?

1.重温优先队列

优先队列在第2章中已经介绍过了。Jarník算法将需要用到优先队列。我们可以从第2章的程序包中导入PriorityQueue类,要获得详情请参阅紧挨着代码清单4-5之前的注意事项,也可以把该类复制为一个新文件并放入本章的程序包中。为完整起见,在代码清单4-9中,我们将重新创建第2章中的PriorityQueue,这里假定import语句会被放入单独的文件中。

代码清单4-9 priority_queue.py

from typing import TypeVar, Generic, List
from heapq import heappush, heappopT = TypeVar('T')
class PriorityQueue(Generic[T]):    def __init__(self) -> None:self._container: List[T] = []@property    def empty(self) -> bool:return not self._container  # not is true for empty containerdef push(self, item: T) -> None:heappush(self._container, item)  # in by prioritydef pop(self) -> T:return heappop(self._container)  # out by prioritydef __repr__(self) -> str:return repr(self._container)

2.计算加权路径的总权重

在开发查找最小生成树的方法之前,我们需要开发一个用于检测某个解的总权重的函数。最小生成树问题的解将由组成树的加权边列表构成。首先,我们会将WeightedPath定义为WeightedEdge的列表,然后会定义一个total_weight()函数,该函数以WeightedPath的列表为参数并把所有边的权重相加,以便得到总权重。具体代码如代码清单4-10所示。

代码清单4-10 mst.py

from typing import TypeVar, List, Optional
from weighted_graph import WeightedGraph
from weighted_edge import WeightedEdge
from priority_queue import PriorityQueue
V = TypeVar('V') # type of the vertices in the graph
WeightedPath = List[WeightedEdge] # type alias for paths
def total_weight(wp: WeightedPath) -> float:return sum([e.weight for e in wp])

3.Jarník算法

查找最小生成树的Jarník算法把图分为两部分:正在生成的最小生成树的顶点和尚未加入最小生成树的顶点。其工作步骤如下所示。

(1)选择要被包含于最小生成树中的任一顶点。

(2)找到连通最小生成树与尚未加入树的顶点的权重最小的边。

(3)将权重最小边末端的顶点添加到最小生成树中。

(4)重复第2步和第3步,直到图中的每个顶点都加入了最小生成树。

注意 Jarník算法常被称为Prim算法。在20世纪20年代末,两位捷克数学家OtakarBorůvka和VojtěchJarník致力于尽量降低铺设电线的成本,提出了解决最小生成树问题的算法。他们提出的算法在几十年后又被其他人“重新发现”[3]。

为了高效地运行Jarník算法,需要用到优先队列。每次将新的顶点加入最小生成树时,所有连接到树外顶点的出边都会被加入优先队列中。从优先队列中弹出的一定是权重最小的边,算法将持续运行直至优先队列为空为止。这样就确保了权重最小的边一定会优先加入树中。如果被弹出的边与树中的已有顶点相连,则它将被忽略。

代码清单4-11中的mst()完整实现了Jarník算法[4],它还带了一个用来打印WeightedPath的实用函数。

警告 Jarník算法在有向图中不一定能正常工作,它也不适用于非连通图。

代码清单4-11 mst.py(续)

def mst(wg: WeightedGraph[V], start: int = 0) -> Optional[WeightedPath]:if start > (wg.vertex_count - 1) or start < 0:return Noneresult: WeightedPath = [] # holds the final MSTpq: PriorityQueue[WeightedEdge] = PriorityQueue()    visited: [bool] = [False] * wg.vertex_count # where we've beendef visit(index: int):        visited[index] = True # mark as visitedfor edge in wg.edges_for_index(index): # add all edges coming from here to pq            if not visited[edge.v]:pq.push(edge)    visit(start) # the first vertex is where everything beginswhile not pq.empty: # keep going while there are edges to processedge = pq.pop()        if visited[edge.v]:continue # don't ever revisit# this is the current smallest, so add it to solution        result.append(edge)         visit(edge.v) # visit where this connectsreturn result
def print_weighted_path(wg: WeightedGraph, wp: WeightedPath) -> None:for edge in wp:print(f"{wg.vertex_at(edge.u)} {edge.weight}> {wg.vertex_at(edge.v)}")print(f"Total Weight: {total_weight(wp)}")

下面逐行过一遍mst()。

def mst(wg: WeightedGraph[V], start: int = 0) -> Optional[WeightedPath]:if start > (wg.vertex_count - 1) or start < 0:return None

本算法将返回某一个代表最小生成树的WeightedPath对象。运算本算法的起始位置无关紧要(假定图是连通和无向的),因此默认设为索引为0的顶点。如果start无效,则mst()返回None。

result: WeightedPath = [] # holds the final MST
pq: PriorityQueue[WeightedEdge] = PriorityQueue()visited: [bool] = [False] * wg.vertex_count # where we've been

result将是最终存放加权路径的地方,也即包含了最小生成树。随着权重最小的边不断被弹出以及图中新的区域不断被遍历,WeightedEdge会不断被添加到result中。因为Jarník算法总是选择权重最小的边,所以被视为贪婪算法(greedy algorithm)之一。pq用于存储新发现的边并弹出次低权重的边。visited用于记录已经到过的顶点索引,这用Set也可以实现,类似于bfs()中的explored。

def visit(index: int):visited[index] = True # mark as visitedfor edge in wg.edges_for_index(index): # add all edges coming from hereif not visited[edge.v]:pq.push(edge)

visit()是一个便于内部使用的函数,用于把顶点标记为已访问,并把尚未访问过的顶点所连的边都加入pq中。不妨注意一下,使用邻接表模型能够轻松地找到属于某个顶点的边。

visit(start) # the first vertex is where everything begins

除非图是非连通的,否则先访问哪个顶点是无所谓的。如果图是非连通的,是由多个不相连的部分组成的,那么mst()返回的树只会涵盖图的某一部分,也就是起始节点所属的那部分图。

while not pq.empty: # keep going while there are edges to processedge = pq.pop()if visited[edge.v]:continue # don't ever revisit# this is the current smallest, so add itresult.append(edge) visit(edge.v) # visit where this connects
return result

只要优先队列中还有边存在,我们就将它们弹出并检查它们是否会引出尚未加入树的顶点。因为优先队列是以升序排列的,所以会先弹出权重最小的边。这就确保了结果确实具有最小总权重。如果弹出的边不会引出未探索过的顶点,那么就会被忽略,否则,因为该条边是目前为止权重最小的边,所以会被添加到结果集中,并且对其引出的新顶点进行探索。如果已没有边可供探索了,则返回结果。

最后再回到用轨道最少的超级高铁网络连接美国15个最大的MSA的问题吧。结果路径就是city_graph2的最小生成树。下面尝试对city_graph2运行一下mst(),具体代码如代码清单4-12所示。

代码清单4-12 mst.py(续)

if __name__ == "__main__":city_graph2: WeightedGraph[str] = WeightedGraph(["Seattle", "San Francisco", "Los Angeles", "Riverside", "Phoenix", "Chicago", "Boston", "New York", "Atlanta","Miami", "Dallas", "Houston", "Detroit", "Philadelphia", "Washington"])city_graph2.add_edge_by_vertices("Seattle", "Chicago", 1737)city_graph2.add_edge_by_vertices("Seattle", "San Francisco", 678)city_graph2.add_edge_by_vertices("San Francisco", "Riverside", 386)city_graph2.add_edge_by_vertices("San Francisco", "Los Angeles", 348)city_graph2.add_edge_by_vertices("Los Angeles", "Riverside", 50)city_graph2.add_edge_by_vertices("Los Angeles", "Phoenix", 357)city_graph2.add_edge_by_vertices("Riverside", "Phoenix", 307)city_graph2.add_edge_by_vertices("Riverside", "Chicago", 1704)city_graph2.add_edge_by_vertices("Phoenix", "Dallas", 887)city_graph2.add_edge_by_vertices("Phoenix", "Houston", 1015)city_graph2.add_edge_by_vertices("Dallas", "Chicago", 805)city_graph2.add_edge_by_vertices("Dallas", "Atlanta", 721)city_graph2.add_edge_by_vertices("Dallas", "Houston", 225)city_graph2.add_edge_by_vertices("Houston", "Atlanta", 702)city_graph2.add_edge_by_vertices("Houston", "Miami", 968)city_graph2.add_edge_by_vertices("Atlanta", "Chicago", 588)city_graph2.add_edge_by_vertices("Atlanta", "Washington", 543)city_graph2.add_edge_by_vertices("Atlanta", "Miami", 604)city_graph2.add_edge_by_vertices("Miami", "Washington", 923)city_graph2.add_edge_by_vertices("Chicago", "Detroit", 238)city_graph2.add_edge_by_vertices("Detroit", "Boston", 613)city_graph2.add_edge_by_vertices("Detroit", "Washington", 396)city_graph2.add_edge_by_vertices("Detroit", "New York", 482)city_graph2.add_edge_by_vertices("Boston", "New York", 190)city_graph2.add_edge_by_vertices("New York", "Philadelphia", 81)city_graph2.add_edge_by_vertices("Philadelphia", "Washington", 123)result: Optional[WeightedPath] = mst(city_graph2)    if result is None:print("No solution found!")else:print_weighted_path(city_graph2, result)

好在有美观打印方法printWeightedPath(),最小生成树的可读性很不错。

Seattle 678> San Francisco
San Francisco 348> Los Angeles
Los Angeles 50> Riverside
Riverside 307> Phoenix
Phoenix 887> Dallas
Dallas 225> Houston
Houston 702> Atlanta
Atlanta 543> Washington
Washington 123> Philadelphia
Philadelphia 81> New York
New York 190> Boston
Washington 396> Detroit
Detroit 238> Chicago
Atlanta 604> Miami
Total Weight: 5372

换句话说,这是加权图中连通所有MSA的总边长最短的组合,至少需要轨道8645 km。图4-7呈现了这棵最小生成树。

图4-7 高亮的边代表连通全部15个MSA的最小生成树

本文摘自《算法精粹:经典计算机科学问题的Python实现》,大卫·科帕克(David Kopec) 著,戴旭 译。

本书是一本面向中高级程序员的算法教程,借助Python语言,用经典的算法、编码技术和原理来求解计算机科学的一些经典问题。全书共9章,不仅介绍了递归、结果缓存和位操作等基本编程组件,还讲述了常见的搜索算法、常见的图算法、神经网络、遗传算法、k均值聚类算法、对抗搜索算法等,运用了类型提示等Python高级特性,并通过各级方案、示例和习题展开具体实践。

本书将计算机科学与应用程序、数据、性能等现实问题深度关联,定位独特,示例经典,适合有一定编程经验的中高级Python程序员提升用Python解决实际问题的技术、编程和应用能力。

假设将15个MSA连入超级高铁网络,如何用最少的轨道连接所有MSA?相关推荐

  1. 立志入行网络或已经初入行的朋友的建议

    今天有空,感觉和SPOTO有缘,随便写点东西给大家.很久没有混在技术论坛中,记忆里最近一次大概是在5.6年以前. 大学读的是土木,曾经也在建筑工地和设计院实习,我相信我可以把那些事情做好,但完全不是我 ...

  2. (转)给想立志入行网络或已经初入行的朋友的建议

    今天有空,感觉和SPOTO有缘,随便写点东西给大家.很久没有混在技术论坛中,记忆里最近一次大概是在5.6年以前. 大学读的是土木,曾经也在建筑工地和设计院实习,我相信我可以把那些事情做好,但完全不是我 ...

  3. 没有找到MSVCP71.dll,迅雷5无法进行离线下载,P2P Seacher无法连入emule网络

    没有找到MSVCP71.dll,迅雷5无法进行离线下载,P2P Seacher无法连入emule网络 2012年12月7日14:14:08 某同学重装XP系统后,使用P2P Seacher绿色版来找片 ...

  4. 第二章教程15:事件系统初入

    本次教程内容: 模块化的编程思维 事件概念的详细分析 字符串处理函数 通用的算法结构 能从架构上解决的问题,才是真正的解决了.凑合上的"解决"方案,充其量只是延缓了系统的崩溃,从而 ...

  5. 软工网络15个人阅读作业2 201521123023 网络1511 戴建钊

    提出问题 快速通读教材<构建之法>,并参照提问模板,提出5个问题. 如何提出有价值的问题? 请看这个文章:http://www.cnblogs.com/rocedu/p/5167941.h ...

  6. 15 | 二分查找(上):如何用最省内存的方式实现快速查找功能?

    思考题:假设有 1000 万个整数数据,每个数据占 8 个字节,如何设计数据结构和算法,快速判断某个整数是否出现在这 1000 万数据中?希望不要占用太多的内存空间,最多不要超过 100MB 二分思想 ...

  7. [连载型] Neutron 系列 (15): OpenStack 是如何实现 Neutron 网络 和 Nova虚机 防火墙的...

    问题导读: 1.Nova安全组是什么? 2.Nova的是如何配置的? 3.FWaas是什么? 1. Nova 安全组 1.1 配置 节点 配置文件 配置项 说明 controller /etc/nov ...

  8. 详解在VMware Workstation Pro 15 安装 CentOS 7后配置网络

    详解在VMware Workstation Pro 15 安装 CentOS7后配置网络 在Windows 10 64bit上安装VMware Workstation Pro 15后,在虚拟机中安装了 ...

  9. 15款语言学习2.0网络服务

    语言的学习,除了平时基本的普通的听说读写练习外,加上网络的帮助或许对您来说会事半功倍,下面就为大家总结15款对您进行语言学习会有所帮助的网络服务及应用.包括语言学习 SNS.单词学习与记忆.互助翻译等 ...

最新文章

  1. SpringBoot实现微信点餐
  2. 开机自动挂载与autofs触发挂载
  3. 适合初学者快速入门的Numpy实战全集
  4. Authentication和Authorization的区别
  5. linux头文件 库,Linux操作系统的头文件和库文件搜索路径
  6. 【小白学云计算】xmpp开源服务器的配置和安装图文详解
  7. WebAPI Delete方法报错405 Method Not Allowed
  8. 【数字信号】基于matlab GUI虚拟信号发生器(各种波形)【含Matlab源码 271期】
  9. 宗成庆《自然语言理解》第5章作业
  10. 【剑指Offer(专项突击版)】001~059题目题解汇总
  11. 小杜滑铲 DFS 最长路径
  12. 一键修复手机电池_怎么恢复手机电池寿命(教你一键修复电池损耗)
  13. SQL中查询MySQL的版本
  14. 3.4.8nbsp;拉里·佩奇和谢尔盖·布林
  15. 自我实现tcmalloc的项目简化版本
  16. python splitext()函数
  17. 如果今天是星期三,后天就是星期五;如果今天是星期六,后天就是星期一。我们用数字1到7对应星期一到星期日。给定某一天,请你输出那天的“后天”是星期几。
  18. 智慧校园之物联网平台对接各子系统
  19. html表格合并线条不加粗,word表格线条加粗后不显示
  20. 巨磁阻抗GMI磁传感器模组选型推荐

热门文章

  1. 自己动手写个病毒专杀工具
  2. 软件测试周刊(第58期):春光不必趁早,冬霜不会迟到。相聚离开,全部刚刚好。
  3. Datawhale 知识图谱组队学习 之 Task 4 用户输入->知识库的查询语句
  4. 面临公司变相裁员该如何应对
  5. 苹果以旧换新价格表_苹果以旧换新价格表最新 iPhone以旧换新折抵价下降
  6. 苹果CEO去年年收入公布:让人望尘莫及
  7. keytool list -rfc -keystore 查看证书信息
  8. 直播回顾 | 第四期直播课堂:5G消息在工业领域的应用分享
  9. 机器人与控制器的关联
  10. 论计算机科学与仿生学论文,仿生学论文-学术参考网