我从 Gareth Rees 那里得到了这个非常棒的答案,它完全涵盖了这个问题。
- 审核
bfs_visited 的文档字符串应该解释节点参数。
compute_resilience 的文档字符串应说明 ugraph 参数已被修改。或者,该函数可以获取图形的副本,以便不修改原始图形。
在 bfs_visited 中:
queue = deque()
queue.append(node)
can be simplified to:
queue = deque([node])
函数largest_cc_size 建立一个配对列表:
res = [(len(ccc), ccc) for ccc in cc_visited(ugraph)]
res.sort()
return res[-1][0]
但是你可以看到它只使用每对的第一个元素(组件的大小)。因此,您可以通过不构建对来简化它:
res = [len(ccc) for ccc in cc_visited(ugraph)]
res.sort()
return res[-1]
由于只需要最大组件的大小,因此无需构建整个列表。相反,您可以使用 max 来找到最大的:
if ugraph:
return max(map(len, cc_visited(ugraph)))
else:
return 0
如果您使用的是 Python 3.4 或更高版本,则可以使用 max 的默认参数进一步简化:
return max(map(len, cc_visited(ugraph)), default=0)
现在很简单,它可能不需要自己的功能。
这一行:
remaining_nodes = set(graph.keys())
可以写得更简单:
remaining_nodes = set(graph)
在设置的剩余节点上存在一个循环,在每次循环迭代中您更新剩余节点:
for node in remaining_nodes:
visited = bfs_visited(graph, node)
if visited not in connected_components:
connected_components.append(visited)
remaining_nodes = remaining_nodes - visited
看起来代码的意图是通过从剩余节点中删除它们来避免迭代访问的节点,但这不起作用!问题在于 for 语句:
for node in remaining_nodes:
仅在循环开始时计算表达式剩余节点一次。所以当代码创建一个新集合并将其分配给剩余节点时:
remaining_nodes = remaining_nodes - visited
这对被迭代的节点没有影响。
您可能想尝试通过使用 difference_update 方法来调整被迭代的集合来解决此问题:
remaining_nodes.difference_update(visited)
但这不是一个好主意,因为这样你会遍历一个集合并在循环中修改它,这是不安全的。相反,您需要编写如下循环:
while remaining_nodes:
node = remaining_nodes.pop()
visited = bfs_visited(graph, node)
if visited not in connected_components:
connected_components.append(visited)
remaining_nodes.difference_update(visited)
使用 while 和 pop 是 Python 中用于在修改数据结构的同时使用数据结构的标准习惯用法 - 您在 bfs_visited 中执行类似的操作。
现在不需要测试了:
如果访问不在 connected_components 中:
因为每个组件只生产一次。
compute_resilience 中的第一行是:
res = [len(ugraph)]
但这仅在图形是单个连接组件开始时才有效。要处理一般情况,第一行应该是:
res = [largest_cc_size(ugraph)]
对于攻击顺序中的每个节点,compute_resilience 调用:
res.append(largest_cc_size(ugraph))
但这并没有利用之前完成的工作。当我们从图中删除节点时,所有连接的组件都保持不变,除了包含节点的连接组件。因此,如果我们只对那个组件进行广度优先搜索,而不是对整个图进行搜索,我们可能会节省一些工作。 (这是否真的节省了任何工作取决于图的弹性。对于高弹性的图,它不会有太大的区别。)
为了做到这一点,我们需要重新设计数据结构,以便我们可以有效地找到包含节点的组件,并有效地从组件集合中删除该组件。
这个答案已经很长了,我不会详细解释如何重新设计数据结构,我将给出修改后的代码,让你自己弄清楚。
def connected_components(graph, nodes):
"""Given an undirected graph represented as a mapping from nodes to
the set of their neighbours, and a set of nodes, find the
connected components in the graph containing those nodes.
Returns:
- mapping from nodes to the canonical node of the connected
component they belong to
- mapping from canonical nodes to connected components
"""
canonical = {}
components = {}
while nodes:
node = nodes.pop()
component = bfs_visited(graph, node)
components[node] = component
nodes.difference_update(component)
for n in component:
canonical[n] = node
return canonical, components
def resilience(graph, attack_order):
"""Given an undirected graph represented as a mapping from nodes to
an iterable of their neighbours, and an iterable of nodes, generate
integers such that the the k-th result is the size of the largest
connected component after the removal of the first k-1 nodes.
"""
# Take a copy of the graph so that we can destructively modify it.
graph = {node: set(neighbours) for node, neighbours in graph.items()}
canonical, components = connected_components(graph, set(graph))
largest = lambda: max(map(len, components.values()), default=0)
yield largest()
for node in attack_order:
# Find connected component containing node.
component = components.pop(canonical.pop(node))
# Remove node from graph.
for neighbor in graph[node]:
graph[neighbor].remove(node)
graph.pop(node)
component.remove(node)
# Component may have been split by removal of node, so search
# it for new connected components and update data structures
# accordingly.
canon, comp = connected_components(graph, component)
canonical.update(canon)
components.update(comp)
yield largest()
在修改后的代码中,最大操作必须遍历所有剩余的连接组件才能找到最大的。可以通过将连接的组件存储在优先级队列中来提高此步骤的效率,以便可以及时找到与组件数量成对数的最大的组件。
我怀疑这部分算法在实践中是一个瓶颈,所以可能不值得额外的代码,但如果你需要这样做,那么 Python 文档中有一些优先级队列实现说明。
-
性能比较
下面是一个用于制作测试用例的有用函数:
从 itertools 导入组合
从随机导入随机
def random_graph(n, p):
"""返回一个随机的无向图,有n个节点,每条边都被选中
以概率 p 独立。
"""
assert 0 <= p <= 1
graph = {i: set() for i in range(n)}
for i, j in combinations(range(n), 2):
if random() <= p:
graph[i].add(j)
graph[j].add(i)
return graph
现在,快速比较修改后的代码和原始代码之间的性能。请注意,我们必须先运行修改后的代码,因为原始代码会破坏性地修改图形,如上文第 1.2 节所述。
>>> from timeit import timeit
>>> G = random_graph(300, 0.2)
>>> timeit(lambda:list(resilience(G, list(G))), number=1) # revised
0.28782312001567334
>>> timeit(lambda:compute_resilience(G, list(G)), number=1) # original
59.46968446299434
所以修改后的代码在这个测试用例上快了大约 200 倍。