💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
# 三、小世界图 > 原文:[Chapter 3 Small world graphs](http://greenteapress.com/complexity2/html/thinkcomplexity2004.html) > 译者:[飞龙](https://github.com/wizardforcel) > 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) > 自豪地采用[谷歌翻译](https://translate.google.cn/) 现实世界中的许多网络,包括社交网络在内,具有“小世界属性”,即节点之间的平均距离,以最短路径上的边数来衡量,远远小于预期。 在本章中,我介绍了斯坦利·米拉格(Stanley Milgram)的著名的“小世界实验”,这是小世界属性在真正的社交网络中的第一次科学演示。之后我们将考虑 Watts-Strogatz 图,它是一个小世界图的模型。我将复制 Watts 和 Strogatz 所做的实验,并解释它打算展示的东西。 这个过程中,我们将看到两种新的图算法:广度优先搜索(BFS)和 Dijkstra 算法,用于计算图中节点之间的最短路径。 本章的代码在本书仓库的`chap03.ipynb`中。使用代码的更多信息请参见第(?)章。 ## 3.1 Stanley Milgram 斯坦利·米拉格(Stanley Milgram)是美国社会心理学家,他进行了两项最著名的社会科学实验,即 Milgram 实验,研究人们对权威的服从(<http://en.wikipedia.org/wiki/Milgram_experiment>)和小世界实验,研究了社交网络的结构(<http://en.wikipedia.org/wiki/Small_world_phenomenon>)。 在小世界实验中,Milgram 向堪萨斯州威奇托(Wichita, Kansas)的几个随机选择的人发送了包裹,带有一个指示,要求他们向马萨诸塞州沙龙(Sharon, Massachusetts)的目标人员发送一封附带的信(在我长大的地方,波士顿附近),目标人员通过名字和职业确定。受访者被告知,只有当他亲自认识目标人员时,才可以将该信直接邮寄给目标;否则他们按照指示,将信和同一个指示发送给他们认为的,更有可能认识目标人员的亲戚或朋友。 许多信件从来没有发出过,但是对于发出的信件,平均路径长度(信件转发次数)的大约为 6。这个结果用于确认以前的观察(和猜测),社交网络中任何两个人之间的通常距离是“六度分隔”。 这个结论令人惊讶,因为大多数人都希望社交网络本地化 - 人们往往会靠近他们的朋友 - 而且在一个具有本地连接的图中,路径长度往往会与地理距离成比例增加。例如,我的大多数朋友都住在附近,所以我猜想社交网络中节点之间的平均距离是大约 50 英里。威奇托距离波士顿约有 1600 英里,所以如果 Milgram 的信件穿过了社交网络的典型环节,那么他们应该有 32 跳,而不是 6 跳。 ## 3.2 Watts 和 Strogatz 1998年,Duncan Watts 和 Steven Strogatz 在 Nature 杂志上发表了一篇“小世界网络的集体动态”(Collective dynamics of ’small-world’ networks)论文,提出了小世界现象的解释。 你可以从 <http://www.nature.com/nature/journal/v393/n6684/abs/393440a0.html> 下载。 Watts 和 Strogatz 从两种很好理解的图开始:随机图和正则图。在随机图中,节点随机连接。在正则图中,每个节点具有相同数量的邻居。他们考虑这些图的两个属性,群聚性和路径长度: 群聚是图表的“集团性”(cliquishness)的度量。在图中,集团是所有节点的子集,它们彼此连接;在一个社交网络中,集团是一群人,彼此都是朋友。Watts 和 Strogatz 定义了一个群聚系数,用于量化两个节点彼此连接,并同时连接到同一个节点的可能性。 路径长度是两个节点之间的平均距离的度量,对应于社交网络中的分离度。 Watts 和 Strogatz 表明,正则图具有高群聚性和长路径长度,而大小相同的随机图通常具有群聚性和短路径长度。所以这些都不是一个很好的社交网络模型,它是高群聚性与短路径长度的组合。 他们的目标是创造一个社交网络的生成模型。生成模型通过为构建或导致现象的过程建模,试图解释现象。Watts 和 Strogatz 提出了用于构建小世界图的过程: 1. 从一个正则图开始,节点为`n`,每个节点连接`k`个邻居。 2. 选择边的子集,并将它们替换为随机的边来“重新布线”。 边的重新布线的概率是参数`p`,它控制图的随机性。当`p = 0`时,该图是正则的;`p = 1`是随机的。 Watts 和 Strogatz 发现,较小的`p`值产生高群聚性的图,如正则图,短路径长度的图,如随机图。 在本章中,我将按以下步骤复制 Watts 和 Strogatz 实验: + 我们将从构建一个环格(ring lattice)开始,这是一种正则图。 + 然后我们和 Watts 和 Strogatz 一样重新布线。 + 我们将编写一个函数来测量群聚度,并使用 NetworkX 函数来计算路径长度。 + 然后,我们为范围内的`p`值计算群聚度和路径长度。 + 最后,我将介绍一种用于计算最短路径的高效算法,Dijkstra 算法。 ## 3.3 环格 ![](https://img.kancloud.cn/af/1e/af1ebc331330d13995dbc3876c74a61f_354x238.png) > 图 3.1 `n=10`,`k=4`的环格 正则图是每个节点具有相同数量的邻居的图;邻居的数量也称为节点的度。 环格是一种正则图,Watts 和 Strogatz 将其用作模型的基础。 在具有`n`个节点的环格中,节点可以排列成圆形,每个节点连接`k`个最近邻居。 例如,`n = 3`和`k = 2`的环形网格将拥有以下边:`(0, 1), (1, 2), (2, 0)`。 请注意,边从编号最高的节点“绕回”0。 更一般地,我们可以像这样枚举边: ```py def adjacent_edges(nodes, halfk): n = len(nodes) for i, u in enumerate(nodes): for j in range(i+1, i+halfk+1): v = nodes[j % n] yield u, v ``` `adjacent_edges`接受节点列表和参数`halfk`,它是`k`的一半。它是一个生成器函数,一次产生一个边。它使用模运算符`%`,从编号最高的节点绕回最低的节点。 我们可以这样测试: ```py >>> nodes = range(3) >>> for edge in adjacent_edges(nodes, 1): ... print(edge) (0, 1) (1, 2) (2, 0) ``` 现在我们可以使用`adjacent_edges`来生成环格。 ```py def make_ring_lattice(n, k): G = nx.Graph() nodes = range(n) G.add_nodes_from(nodes) G.add_edges_from(adjacent_edges(nodes, k//2)) return G ``` 注意,`make_ring_lattice`使用地板除计算`halfk`,所以如果`k`是奇数,它将向下取整并产生具有度`k-1`的环格。这可能不是我们想要的,但现在还不错。 我们可以像这样测试函数: ```py lattice = make_ring_lattice(10, 4) ``` 图(?)展示了结果。 ## 3.4 WS 图 ![](https://img.kancloud.cn/ab/29/ab29a98e59376cfa6c204937c8025e7c_354x180.png) > 图 3.2 WS 图,`n=20`,`k=4`,`p=0`(左边),`p=0.2`(中间),`p=1`(右边)。 为了制作 Watts-Strogatz(WS)图,我们从一个环格开始,并为一些边“重新布线”。 在他们的论文中,Watts 和 Strogatz 以特定顺序考虑边,并用概率`p`重新布置每个边。 如果边被重新布置,则它们使第一个节点保持不变,并随机选择第二个节点。它们不允许自环或多边;也就是说,节点不能拥有到它自身的边,并且两个节点之间不能拥有多个边。 这是我的这个过程的实现。 ```py def rewire(G, p): nodes = set(G.nodes()) for edge in G.edges(): if flip(p): u, v = edge choices = nodes - {u} - set(G[u]) new_v = choice(tuple(choices)) G.remove_edge(u, v) G.add_edge(u, new_v) ``` 参数`p`是边的重新布线的概率。`for`循环枚举了边,并使用`flip`,它以概率`p`返回`True`,来选择哪些被重新布置。 如果我们重新布置节点`u`到节点`v`的边,我们必须选择一个节点来替换`v`,称为`new_v`。为了计算可能的选择,我们从节点集开始,它是一个集合,并且移除`u`和它的邻居,这避免了自环和多边。 然后我们从选项中选择new_v,将`u`到`v`的现有删除,并从添加一个`u`到`new_v`的新边。 另外,表达式`G[u]`返回一个字典,他的键是包含`u`的邻居。在这种情况下,它比使用`G.neighbors`更快一点。 这个函数不按照 Watts 和 Strogatz 指定的顺序考虑边缘,但它似乎不会影响结果。 图(?)展示了`n = 20`,`k = 4`和范围内`p`值的 WS 图。当`p = 0`时,该图是环格。 当`p = 1`时,它是完全随机的。我们将看到,有趣的事情发生在两者之间。 ## 3.5 群聚性 下一步是计算群聚系数,它量化了节点形成集团的趋势。 集团是一组完全连接的节点;也就是说,在集团中的所有节点对之间都存在边。 假设一个特定的节点`u`具有`k`个邻居。如果所有的邻居都相互连接,则会有`k(k-1)/2`个边。 实际存在的这些边的比例是`u`的局部群聚系数,表示为`Cu`。它被称为“系数”,因为它总是在 0 和 1 之间。 如果我们计算所有节点上的`Cu`平均值,我们得到“网络平均群聚系数”,表示为`C`。 这是一个计算它的函数。 ```py def node_clustering(G, u): neighbors = G[u] k = len(neighbors) if k < 2: return 0 total = k * (k-1) / 2 exist = 0 for v, w in all_pairs(neighbors): if G.has_edge(v, w): exist +=1 return exist / total ``` 同样,我使用`G [u]`,它返回一个字典,键是节点的邻居。如果节点的邻居少于两个,则群聚系数未定义,但为简便起见,`node_clustering`返回 0。 否则,我们计算邻居之间的可能的边数量,`total`,然后计算实际存在的边数量。结果是存在的所有边的比例。 我们可以这样测试函数: ```py >>> lattice = make_ring_lattice(10, 4) >>> node_clustering(lattice, 1) 0.5 ``` 在`k=4`的环格中,每个节点的群聚系数是`0.5`(如果你不相信,可以看看图(?))。 现在我们可以像这样计算网络平均群聚系数: ```py def clustering_coefficient(G): cc = np.mean([node_clustering(G, node) for node in G]) return cc ``` `np.mean` 是个 NumPy 函数,计算列表或数组中元素的均值。 然后我们可以像这样测试: ```py >>> clustering_coefficient(lattice) 0.5 ``` 这个图中,所有节点的局部群聚系数是 0.5,所以节点的平均值是 0.5。当然,我们期望这个值和 WS 图不同。 ## 3.6 最短路径长度 下一步是计算特征路径长度`L`,它是每对节点之间最短路径的平均长度。 为了计算它,我将从 NetworkX 提供的函数开始,`shortest_path_length`。 我会用它来复制 Watts 和 Strogatz 实验,然后我将解释它的工作原理。 这是一个函数,它接受图并返回最短路径长度列表,每对节点一个。 ```py def path_lengths(G): length_map = nx.shortest_path_length(G) lengths = [length_map[u][v] for u, v in all_pairs(G)] return lengths ``` `nx.shortest_path_length`的返回值是字典的字典。外层字典每个节点`u`到内层字典的映射,内层字典是每个节点`v`到`u->v`的最短路径长度的映射。 使用来自`path_lengths`的长度列表,我们可以像这样计算`L`: ```py def characteristic_path_length(G): return np.mean(path_lengths(G)) ``` 并且我们可以使用小型的环格来测试它。 ```py >>> lattice = make_ring_lattice(3, 2) >>> characteristic_path_length(lattice) 1.0 ``` 这个例子中,所有三个节点都互相连接,所以平均长度为 1。 ## 3.7 WS 实验 ![](https://img.kancloud.cn/58/21/582107b4b70e1998d55c07a61c0ef1b0_354x238.png) > 图 3.3:WS 图的群聚系数`C`和特征路径长度`L`,其中`n=1000, k=10`,`p`是一个范围。 现在我们准备复制 WS 实验,它表明对于一系列`p`值,WS 图具有像正则图像那样的高群聚性,像随机图一样的短路径长度。 我将从`run_one_graph`开始,它接受`n`,`k`和`p`;它生成具有给定参数的 WS图,并计算平均路径长度`mpl`和群聚系数`cc`: ```py def run_one_graph(n, k, p): ws = make_ws_graph(n, k, p) mpl = characteristic_path_length(ws) cc = clustering_coefficient(ws) print(mpl, cc) return mpl, cc ``` Watts 和 Strogatz 用`n = 1000`和`k = 10`进行实验。使用这些参数,`run_one_graph`在我的电脑上需要大约一秒钟;大部分时间用于计算平均路径长度。 现在我们需要为范围内的`p`计算这些值。我将再次使用 NumPy 函数`logspace`来计算`ps`: ```py ps = np.logspace(-4, 0, 9) ``` 对于每个`p`的值,我生成了 3 个随机图,并且我们将结果平均。这里是运行实验的函数: ```py def run_experiment(ps, n=1000, k=10, iters=3): res = {} for p in ps: print(p) res[p] = [] for _ in range(iters): res[p].append(run_one_graph(n, k, p)) return res ``` 结果是个字典,将每个`p`值映射为`(mpl, cc)`偶对的列表。 最后一步就是聚合结果: ```py L = [] C = [] for p, t in sorted(res.items()): mpls, ccs = zip(*t) mpl = np.mean(mpls) cc = np.mean(ccs) L.append(mpl) C.append(cc) ``` 每次循环时,我们取得一个`p`值和一个`(mpl, cc)`偶对的列表。 我们使用`zip`来提取两个列表,`mpls`和`ccs`,然后计算它们的均值并将它们添加到`L`和`C`,这是路径长度和群聚系数的列表。 为了在相同的轴上绘制`L`和`C`,我们通过除以第一个元素,将它们标准化: ```py L = np.array(L) / L[0] C = np.array(C) / C[0] ``` 图(?)展示了结果。 随着`p`的增加,平均路径长度迅速下降,因为即使少量随机重新布线的边,也提供了图区域之间的捷径,它们在格中相距很远。另一方面,删除局部链接降低了群聚系数,但是要慢得多。 因此,存在较宽范围的`p`,其中 WS 图具有小世界图的性质,高群聚度和短路径长度。 这就是为什么 Watts 和 Strogatz 提出了 WS 图,作为展示小世界现象的,现实世界网络的模型。 ## 3.8 能有什么解释? 如果你问我,为什么行星轨道是椭圆形的,我最开始会为一个行星和一个恒星建模;我将在 <http://en.wikipedia.org/wiki/Newton's_law_of_universal_gravitation> 上查找万有引力定律,并用它为行星的运动写出一个微分方程。之后我会扩展轨道方程式,或者更有可能在 <http://en.wikipedia.org/wiki/Orbit_equation> 上查找。通过一个小的代数运算,我可以得出产生椭圆轨道的条件。之后我会证明我们看做行星的物体满足这些条件。 人们,或至少是科学家,一般对这种解释感到满意。它有吸引力的原因之一是,模型中的假设和近似值似乎是合理的。行星和恒星不是真正的质点,但它们之间的距离是如此之大,以至于它们的实际尺寸可以忽略不计。同一太阳系中的行星可以影响彼此的轨道,但效果通常较小。而且我们忽视相对论的影响,再次假定它们很小。 这也因为它是基于方程式的。我们可以用闭式表达轨道方程,这意味着我们可以有效地计算轨道。这也意味着我们可以得出轨道速度,轨道周期和其他数量的一般表达式。 最后,我认为这是因为它具有数学证明的形式。它从一组公理开始,通过逻辑和分析得出结果。但重要的是要记住,证明属于模型,而不是现实世界。也就是说,我们可以证明,行星的理想模型产生一个椭圆轨道,但是我们不能证明这个模型与实际的行星有关(实际上它不是)。 + 这些模型可以做什么工作:它们是预测性的还是说明性的,还是都有? + 这些模型的解释,是否比基于更传统模型的解释更不满意?为什么? + 我们应该如何刻画这些和更传统的模型之间的差异?他们在种类还是程度上不同? 在这本书中,我将提供我对这些问题的回答,但它们是暂时性的,有时是投机性的。我鼓励你怀疑地思考他们,并得出你自己的结论。 ## 3.9 广度优先搜索 当我们计算最短路径时,我们使用了 NetworkX 提供的一个函数,但是我没有解释它是如何工作的。为此,我将从广度优先搜索开始,这是用于计算最短路径的 Dijkstra 算法的基础。 在第(?)节,我提出了`reachable_nodes`,它寻找从给定的起始节点可以到达的所有节点: ```py def reachable_nodes(G, start): seen = set() stack = [start] while stack: node = stack.pop() if node not in seen: seen.add(node) stack.extend(G.neighbors(node)) return seen ``` 我当时没有这么说,但它执行深度优先搜索(DFS)。现在我们将修改它来执行广度优先搜索(BFS)。 为了了解区别,想象一下你正在探索一座城堡。你最开始在一个房间里,带有三个门,标记为 A,B 和 C 。你打开门 C 并发现另一个房间,它的门被标记为 D ,E 和 F。 下面你打开哪个门呢?如果你打算冒险,你可能想更深入城堡,选择 D,E 或 F。这是一个深度优先搜索。 但是,如果你想更系统化,你可以在 D,E 和 F 之前回去探索 A 和 B,这将是一个广度优先搜索。 在`reachable_nodes`中,我们使用`list.pop`选择下一个节点来“探索”。默认情况下,`pop`返回列表的最后一个元素,这是我们添加的最后一个元素。在这个例子中,这是门 F。 如果我们要执行 BFS,最简单的解决方案是将第一个元素从栈中弹出: ```py node = stack.pop(0) ``` 这有效,但速度很慢。在 Python 中,弹出列表的最后一个元素需要常数时间,但是弹出第一个元素线性于列表的长度。在最坏的情况下,就是堆栈的长度`O(n)`,这使得 BFS 的`O(nm)`的实现比`O(n + m)`差得多。 我们可以用双向队列(也称为`deque`)来解决这个问题。`deque`的一个重要特征就是,你可以在开头和末尾添加和删除元素。要了解如何实现,请参阅 <https://en.wikipedia.org/wiki/Double-ended_queue>。 Python 在`collections`模块中提供了`deque`,所以我们可以像这样导入它: ```py from collections import deque ``` 我们可以使用它来编写高效的 BFS: ```py def reachable_nodes_bfs(G, start): seen = set() queue = deque([start]) while queue: node = queue.popleft() if node not in seen: seen.add(node) queue.extend(G.neighbors(node)) return seen ``` 差异在于: + 我用名为`queue`的`deque`替换了名为`stack`的列表。 + 我用`popleft`替换`pop`,它删除并返回队列的最左边的元素,这是第一个添加的元素。 这个版本恢复为`O(n + m)`。现在我们做好了寻找最短路径的准备。 ## 3.10 (简化的)Dijkstra 算法 Edsger W. Dijkstra 是荷兰计算机科学家,发明了一种有效的最短路径算法(参见 <http://en.wikipedia.org/wiki/Dijkstra's_algorithm>)。他还发明了信号量,它是一种数据结构,用于协调彼此通信的程序(参见 <http://en.wikipedia.org/wiki/Semaphore_(programming>)和 Downey,《The Little Book of Semaphores》)。 作为一系列计算机科学论文的作者,Dijkstra 是著名(臭名昭著)的。 有些比如“反对 GOTO 语句的案例”(A Case against the GO TO Statement),对编程实践产生了深远的影响。其他比如“真正的计算机科学教学的残酷”(On the Cruelty of Really Teaching Computing Science),很有娱乐性,但效果却不好。 Dijkstra 算法解决了“单源最短路径问题”,这意味着它寻找从给定的“源”节点到图中每个其他节点(或至少每个连接节点)的最小距离。 我们最开始考虑算法的简化版本,所有边的长度相同。更一般的版本适用于任何非负的边的长度。 简化版本类似于第一节中的广度优先搜索 除了我们用称为`dist`的字典替换集合`seen`,该字典将每个节点映射为与源的距离: ```py def shortest_path_dijkstra(G, start): dist = {start: 0} queue = deque([start]) while queue: node = queue.popleft() new_dist = dist[node] + 1 neighbors = set(G[node]) - set(dist) for n in neighbors: dist[n] = new_dist queue.extend(neighbors) return dist ``` 这是它的工作原理: + 最初,队列包含单个元素`start`,`dist`将`start`映射为距离 0(这是`start`到自身的距离)。 + 每次循环中,我们使用`popleft`获取节点,按照添加到队列的顺序。 + 接下来,我们发现节点的所有邻居都没有在`dist`中。 + 由于从起点到节点的距离是`dist [node]`,到任何未访问的邻居的距离是`dist [node] +1`。 + 对于每个邻居,我们向`dist`添加一个条目,然后将邻居添加到队列中。 只有在我们使用 BFS 而不是 DFS 时,这个算法才有效。为什么? 第一次循环中,`node`是`start`,`new_dist`为`1`。所以`start`的邻居距离为 1,并且进入了队列。 当我们处理`start`的邻居时,他们的所有邻居距离为`2`。我们知道,他们中没有一个距离为`1`,因为如果有的话,我们会在第一次迭代中发现它们。 类似地,当我们处理距离为 2 的节点时,我们将他们的邻居的距离设为`3`。我们知道它们中没有一个的距离为`1`或`2`,因为如果有的话,我们将在之前的迭代中发现它们。 等等。如果你熟悉归纳证明,你可以看到这是怎么回事。 但是,在我们开始处理距离为`2`的节点之前,只有我们处理了距离为`1`的所有节点,这个论证才有效,依此类推。这正是 BFS 所做的。 在本章末尾的练习中,你将使用 DFS 编写 Dijkstra 算法的一个版本,以便你有机会看到出现什么问题。 ## 3.11 练习 练习 1: 在一个环格中,每个节点的邻居数量相同。邻居的数量称为节点的度,所有节点的度相同的图称为正则图。 所有环格都是正则的,但不是所有的正则图都是环格。特别地,如果`k`是奇数,则不能构造环格,但是我们可以构建一个正则图。 编写一个名为`make_regular_graph`的函数,该函数接受`n`和`k`,并返回包含`n`个节点的正则图,其中每个节点都有`k`个邻居。如果不可能使用`n`和`k`的给定值来制作正则图,则该函数应该抛出`ValueError`。 练习 2: 我的`reachable_nodes_bfs`实现是有效的,因为它是`O(n + m)`的,但它产生了很多开销,将节点添加到队列中并将其删除。 NetworkX 提供了一个简单,快速的 BFS 实现,可从 GitHub 上的 NetworkX 仓库获取,网址为 <https://github.com/networkx/networkx/blob/master/networkx/algorithms/components/connected.py>。 这里是我修改的一个版本,返回一组节点: ```py def _plain_bfs(G, source): seen = set() nextlevel = {source} while nextlevel: thislevel = nextlevel nextlevel = set() for v in thislevel: if v not in seen: seen.add(v) nextlevel.update(G[v]) return seen ``` 将这个函数与`reachable_nodes_bfs`相比,看看哪个更快。之后看看你是否可以修改这个函数来实现更快的`shortest_path_dijkstra`版本。 练习 3: 下面的 BFS 实现包含两个性能错误。它们是什么?这个算法的实际增长级别是什么? ```py def bfs(top_node, visit): """Breadth-first search on a graph, starting at top_node.""" visited = set() queue = [top_node] while len(queue): curr_node = queue.pop(0) # Dequeue visit(curr_node) # Visit the node visited.add(curr_node) # Enqueue non-visited and non-enqueued children queue.extend(c for c in curr_node.children if c not in visited and c not in queue) ``` 练习 4:在第(?)节中,我说了除非使用 BFS,Dijkstra 算法不能工作。编写一个`shortest_path_dijkstra `的版本,它使用 DFS,并使用一些例子测试它,看看哪里不对。 练习 5: Watts 和 Strogatz 的论文的一个自然问题是,小世界现象是否特定于它的生成模型,或者其他类似模型是否产生相同的定性结果(高群聚和短路径长度)。 为了回答这个问题,选择 WS 模型的一个变体并重复实验。 你可能会考虑两种变体: + 不从常规图开始,从另一个高群聚的图开始。 例如,你可以将节点放置在二维空间中的随机位置,并将每个节点连接到其最近的`k`个邻居。 + 尝试不同种类的重新布线。 如果一系列类似的模型产生类似的行为,我们认为论文的结果是可靠的。 练习 6: Dijkstra 算法解决了“单源最短路径”问题,但为了计算图的特征路径长度,我们其实需要解决“多源最短路径”问题。 当然,一个选择是运行 Dijkstra 算法`n`次,每个起始节点一次。 对于某些应用,这可能够好,但是有更有效的替代方案。 找到一个多源最短路径的算法并实现它。请参阅 <https://en.wikipedia.org/wiki/Shortest_path_problem#All-pairs_shortest_paths>。 将实现的运行时间与运行 Dijkstra 算法`n`次进行比较。哪种算法在理论上更好?哪个在实践中更好?NetworkX 使用了哪一个?