【问题标题】:Python Recursive Depth First Search Performance Tips (Is Cython an option?)Python Recursive Depth First Search Performance Tips(Cython 可以选择吗?)
【发布时间】:2015-01-21 19:56:27
【问题描述】:

我有一个项目,该项目依赖于查找图中最多通过顶点 k 次的所有循环。当然,为了开发,我现在坚持使用 k=1 的情况。我得出的结论是,这个算法作为深度优先搜索对于一个完整的图来说最坏的情况是 O((kn)^(kn)),但我很少在问题的上下文中接近这个上限,所以我会还是想试试这个方法。

我已在项目中实施了以下内容以实现这一目标:

class Graph(object):

...

def path_is_valid(self, current_path):
    """
    :param current_path:
    :return: Boolean indicating a whether the given path is valid
    """
    length = len(current_path)
    if length < 3:
        # The path is too short
        return False

    # Passes through vertex twice... sketchy for general case
    if len(set(current_path)) != len(current_path):
        return False

    # The idea here is take a moving window of width three along the path
    # and see if it's contained entirely in a polygon.
    arc_triplets = (current_path[i:i+3] for i in xrange(length-2))
    for triplet in arc_triplets:
        for face in self.non_fourgons:
            if set(triplet) <= set(face):
                return False

    # This is all kinds of unclear when looking at. There is an edge case
    # pertaining to the beginning and end of a path existing inside of a
    # polygon. The previous filter will not catch this, so we cycle the path
    # and recheck moving window filter.
    path_copy = list(current_path)
    for i in xrange(length):
        path_copy = path_copy[1:] + path_copy[:1]  # wtf
        arc_triplets = (path_copy[i:i+3] for i in xrange(length-2))
        for triplet in arc_triplets:
            for face in self.non_fourgons:
                if set(triplet) <= set(face):
                    return False

    return True

def cycle_dfs(self, current_node,  start_node,  graph, current_path):
    """

    :param current_node:
    :param start_node:
    :param graph:
    :param current_path:
    :return:
    """
    if len(current_path) >= 3:
        last_three_vertices = current_path[-3:]
        previous_three_faces = [set(self.faces_containing_arcs[vertex])
                                for vertex in last_three_vertices]
        intersection_all = set.intersection(*previous_three_faces)
        if len(intersection_all) == 2:
            return []

    if current_node == start_node:
        if self.path_is_valid(current_path):
            return [tuple(shift(list(current_path)))]
        else:
            return []

    else:
        loops = []
        for adjacent_node in set(graph[current_node]):
            current_path.append(adjacent_node)
            graph[current_node].remove(adjacent_node)
            graph[adjacent_node].remove(current_node)
            loops += list(self.cycle_dfs(adjacent_node, start_node,
                                         graph, current_path))
            graph[current_node].append(adjacent_node)
            graph[adjacent_node].append(current_node)
            current_path.pop()
        return loops

path_is_valid() 旨在根据特定于问题的过滤条件减少深度优先搜索所产生的路径数量。我试图合理地解释每个人的目的,但一切都在自己的脑海中更加清晰;如果需要,我很乐意改进 cmets。

我愿意接受任何和所有提高性能的建议,因为如下面的个人资料所示,这就是我花费所有时间的原因。

另外,我即将转向 Cython,但我的代码严重依赖 Python 对象,我不知道这是否是明智之举。任何人都可以阐明这条路线是否对所涉及的许多本机 Python 数据结构有益?我似乎找不到太多关于这方面的信息,如果有任何帮助,我们将不胜感激。

因为我知道人们会问,我已经分析了我的整个项目,这就是问题的根源:

   311         1     18668669 18668669.0     99.6              cycles = self.graph.find_cycles()

这是self.graph.find_cycles()self.path_is_valid() 的行配置输出:

Function: cycle_dfs at line 106
Total time: 11.9584 s 

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
   106                                               def cycle_dfs(self, current_node,  start_node,  graph, current_path):
   107                                                   """
   108                                                   Naive depth first search applied to the pseudo-dual graph of the
   109                                                   reference curve. This sucker is terribly inefficient. More to come.
   110                                                   :param current_node:
   111                                                   :param start_node:
   112                                                   :param graph:
   113                                                   :param current_path:
   114                                                   :return:
   115                                                   """
   116    437035       363181      0.8      3.6          if len(current_path) >= 3:
   117    436508       365213      0.8      3.7              last_three_vertices = current_path[-3:]
   118    436508       321115      0.7      3.2              previous_three_faces = [set(self.faces_containing_arcs[vertex])
   119   1746032      1894481      1.1     18.9                                      for vertex in last_three_vertices]
   120    436508       539400      1.2      5.4              intersection_all = set.intersection(*previous_three_faces)
   121    436508       368725      0.8      3.7              if len(intersection_all) == 2:
   122                                                           return []
   123                                           
   124    437035       340937      0.8      3.4          if current_node == start_node:
   125     34848      1100071     31.6     11.0              if self.path_is_valid(current_path):
   126       486         3400      7.0      0.0                  return [tuple(shift(list(current_path)))]
   127                                                       else:
   128     34362        27920      0.8      0.3                  return []
   129                                           
   130                                                   else:
   131    402187       299968      0.7      3.0              loops = []
   132    839160       842350      1.0      8.4              for adjacent_node in set(graph[current_node]):
   133    436973       388646      0.9      3.9                  current_path.append(adjacent_node)
   134    436973       438763      1.0      4.4                  graph[current_node].remove(adjacent_node)
   135    436973       440220      1.0      4.4                  graph[adjacent_node].remove(current_node)
   136    436973       377422      0.9      3.8                  loops += list(self.cycle_dfs(adjacent_node, start_node,
   137    436973       379207      0.9      3.8                                               graph, current_path))
   138    436973       422298      1.0      4.2                  graph[current_node].append(adjacent_node)
   139    436973       388651      0.9      3.9                  graph[adjacent_node].append(current_node)
   140    436973       412489      0.9      4.1                  current_path.pop()
   141    402187       285471      0.7      2.9              return loops

Function: path_is_valid at line 65
Total time: 1.6726 s

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
65                                               def path_is_valid(self, current_path):
66                                                   """
67                                                   Aims to implicitly filter during dfs to decrease output size. Observe
68                                                   that more complex filters are applied further along in the function.
69                                                   We'd rather do less work to show the path is invalid rather than more,
70                                                   so filters are applied in order of increasing complexity.
71                                                   :param current_path:
72                                                   :return: Boolean indicating a whether the given path is valid
73                                                   """
74     34848        36728      1.1      2.2          length = len(current_path)
75     34848        33627      1.0      2.0          if length < 3:
76                                                       # The path is too short
77        99           92      0.9      0.0              return False
78                                           
79                                                   # Passes through arcs twice... Sketchy for later.
80     34749        89536      2.6      5.4          if len(set(current_path)) != len(current_path):
81     31708        30402      1.0      1.8              return False
82                                           
83                                                   # The idea here is take a moving window of width three along the path
84                                                   # and see if it's contained entirely in a polygon.
85      3041         6287      2.1      0.4          arc_triplets = (current_path[i:i+3] for i in xrange(length-2))
86     20211        33255      1.6      2.0          for triplet in arc_triplets:
87     73574        70670      1.0      4.2              for face in self.non_fourgons:
88     56404        94019      1.7      5.6                  if set(triplet) <= set(face):
89      2477         2484      1.0      0.1                      return False
90                                           
91                                                   # This is all kinds of unclear when looking at. There is an edge case
92                                                   # pertaining to the beginning and end of a path existing inside of a
93                                                   # polygon. The previous filter will not catch this, so we cycle the path
94                                                   # a reasonable amount and recheck moving window filter.
95       564          895      1.6      0.1          path_copy = list(current_path)
96      8028         7771      1.0      0.5          for i in xrange(length):
97      7542        14199      1.9      0.8              path_copy = path_copy[1:] + path_copy[:1]  # wtf
98      7542        11867      1.6      0.7              arc_triplets = (path_copy[i:i+3] for i in xrange(length-2))
99    125609       199100      1.6     11.9              for triplet in arc_triplets:
100    472421       458030      1.0     27.4                  for face in self.non_fourgons:
101    354354       583106      1.6     34.9                      if set(triplet) <= set(face):
102        78           83      1.1      0.0                          return False
103                                           
104       486          448      0.9      0.0          return True

谢谢!

编辑:嗯,经过大量无情的分析后,我能够将运行时间从 12 秒缩短到 ~1.5 秒。

我更改了cycle_dfs()的这一部分

    last_three_vertices = current_path[-3:]
    previous_three_faces = [set(self.faces_containing_arcs[vertex])
                            for vertex in last_three_vertices]
    intersection_all = set.intersection(*previous_three_faces)
    if len(intersection_all) == 2: ...

到这里:

        # Count the number of times each face appears by incrementing values
        # of face_id's
        containing_faces = defaultdict(lambda: 0)
        for face in (self.faces_containing_arcs[v]
                     for v in current_path[-3:]):
            for f in face:
                containing_faces[f] += 1
        # If there's any face_id f that has a value of three, that means that
        # there is one face that all three arcs bound. This is a trivial path
        # so we discard it.
        if 3 in containing_faces.values(): ...

这是由我看到的另一篇关于 Python 字典分配基准测试的帖子所激发的;事实证明,在 dict 中分配和编辑值比添加整数要慢一点(这仍然让我大吃一惊)。除了self.path_is_valid() 的两个新增功能外,我还提出了 12 倍的加速。但是,我们将不胜感激,因为更好的整体性能只会随着输入复杂性的增加而使更难的问题变得更容易。

【问题讨论】:

    标签: python performance optimization graph cython


    【解决方案1】:

    我建议对path_is_valid 进行两项优化。当然,你的主要问题在cycle_dfs,你可能只是需要一个更好的算法。

    1) 避免创建额外的数据结构:

     for i in xrange(length-2):
       for face in self.non_fourgons:
          if (path[i] in face && path[i+1] in face && path[i+2] in face):
            return False
    

    2) 创建一个字典映射点到它们所属的 non_fourgons:

     for i in xrange(length-2):
       for face in self.non_fourgons[ path[i] ]:
          if (path[i+1] in face && path[i+2] in face):
            return False
    

    表达式self.non_fourgons[ p ] 应该返回非四边形的列表 其中包含p 作为成员。这减少了您必须检查的多边形数量。

    【讨论】:

    • 太棒了,谢谢!我一直在寻找不那么糟糕的方法来做这件事,而且我想不出太多。我现在就加进去。
    • 这两项优化带来了 60% 的速度提升!从 12 秒缩短到 4 秒!你就是男人!
    猜你喜欢
    • 2013-10-31
    • 1970-01-01
    • 2020-04-23
    • 2011-02-04
    • 1970-01-01
    • 1970-01-01
    • 2011-02-15
    • 1970-01-01
    • 2018-10-07
    相关资源
    最近更新 更多