【问题标题】:Can this breadth-first search be made faster?这种广度优先搜索可以更快吗?
【发布时间】:2010-12-17 17:13:39
【问题描述】:

我有一个数据集,它是一个大型未加权循环图。循环发生在大约 5-6 条路径的循环中。它由大约 8000 个节点组成,每个节点有 1-6 个(通常大约 4-5 个)连接。我正在做单对最短路径计算,并实现了以下代码来进行广度优先搜索。

from Queue import Queue

q = Queue()
parent = {}
fromNode = 'E1123'
toNode = 'A3455'

# path finding
q.put(fromNode)
parent[fromNode] = 'Root'

while not q.empty():
  # get the next node and add its neighbours to queue
  current = q.get()
  for i in getNeighbours(current):
    # note parent and only continue if not already visited
    if i[0] not in parent:
      parent[i[0]] = current
      q.put(i[0])

  # check if destination
  if current == toNode:
    print 'arrived at', toNode
    break

上面的代码使用 Python 2.6 Queue 模块,getNeighbours() 只是一个子例程,它进行一次 MySQL 调用并将邻居作为元组列表返回,例如(('foo',),('bar',))。 SQL 调用很快。

代码运行良好,但测试到大约 7 层的深度大约需要 20 秒才能运行(2.5GHz Intel 4GB RAM OS X 10.6)

我欢迎任何有关如何改进此代码的运行时间的 cmets。

【问题讨论】:

    标签: python algorithm computer-science breadth-first-search


    【解决方案1】:

    好吧,鉴于对评论的支持,我现在将其作为答案。

    紧密循环中的 SQL 肯定会减慢您的速度。我不在乎电话的速度有多快。想想看——你要求解析一个查询,运行一个查找——尽管如此,它仍然处于一个紧密的循环中。你的数据集是什么样的?您可以将SELECT 整个数据集放到内存中,或者至少在 MySQL 之外使用它吗?

    如果您在内存中使用该数据,您将看到显着的性能提升。

    【讨论】:

    • 辅助,8000 个节点很容易放入内存中。
    • 好电话!带有节点信息的表就是fromNode、toNode的行。我将研究简单地将它加载到内存中。也许只是一个大的字典结构。
    • 作为一个简单的测试,我通过在 CREATE TABLE 定义上简单地使用 ENGINE=MEMORY 将 MySQL 加载到内存中。现在完成相同的代码大约需要 2.5 秒!
    • 聚会有点晚了,但您是否使用准备好的语句进行查询?应该能够减少一点 SQL 处理时间,尤其是在内存变化中。
    【解决方案2】:

    类似这样的:

    #!/usr/bin/env python
    
    from Queue import Queue
    
    def traverse_path(fromNode, toNode, nodes):
        def getNeighbours(current, nodes):
            return nodes[current] if current in nodes else []
    
        def make_path(toNode, graph):
            result = []
            while 'Root' != toNode:
                result.append(toNode)
                toNode = graph[toNode]
            result.reverse()
            return result
    
        q = Queue()
        q.put(fromNode)
        graph = {fromNode: 'Root'}
    
        while not q.empty():
            # get the next node and add its neighbours to queue
            current = q.get()
            for neighbor in getNeighbours(current, nodes):
                # use neighbor only continue if not already visited
                if neighbor not in graph:
                    graph[neighbor] = current
                    q.put(neighbor)
    
            # check if destination
            if current == toNode:
                return make_path(toNode, graph)
        return []
    
    if __name__ == '__main__':
        nodes = {
            'E1123': ['D111', 'D222', 'D333', 'D444'],
            'D111': ['C01', 'C02', 'C04'],
            'D222': ['C11', 'C03', 'C05'],
            'D333': ['C01'],
            'C02': ['B1'],
            'B1': ['A3455']
        }
        result = traverse_path('E1123', 'A3455', nodes)
        print result
    
    ['E1123', 'D111', 'C02', 'B1', 'A3455']
    

    如果您将 SQL 查询替换为列表字典(这将是棘手的部分),您将获得这种性能。

    【讨论】:

    • 真棒休。我一直在使用内存表获得良好的性能。我会尝试下一步。大约有 14K 连接,这最终将成为一个网络应用程序,所以我需要仔细考虑加载到内存中的工作原理..
    • 我发现我的代码与你的 Hugh 相似,但你的代码肯定更优雅。除了依赖于区域设置的“邻居/邻居”拼写之外,我对上述内容的唯一额外建议是在 make_path 中调用 result.reverse() ,因为这会按 fromNode -> toNode 的顺序返回一个列表
    【解决方案3】:

    我敢打赌那台机器有不止一个内核,不是吗?并行运行它。

    Python Threading

    【讨论】:

    • 我认为你的意思是 Python 多处理。
    【解决方案4】:

    嗯,BFS 不涉及标记您已经看过的节点,以便您不再访问它们吗?

    【讨论】:

    • 确实如此。如果该节点已经被放在了parent[]中,那么它之前已经被看到过,所以它不会被放入队列中再次执行。
    • 哦,我明白了。我错过了,对不起。尽管如此,将标志放入每个节点比操纵不断增长的集合更便宜。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-01-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-02-16
    相关资源
    最近更新 更多