【问题标题】:DFS algorithm in Python with generators带有生成器的 Python 中的 DFS 算法
【发布时间】:2026-01-05 02:20:11
【问题描述】:

背景:

我正在做一个项目,我需要为文本处理编写一些规则。在为这个项目工作了几天并实施了一些规则之后,我意识到我需要确定规则的顺序。没问题,我们有拓扑排序来帮忙。但后来我意识到我不能指望图表总是满的。所以我想出了这个想法,给定具有一组依赖项(或单个依赖项)的单个规则,我需要检查依赖项的依赖关系。听起来很熟悉?是的。这个主题与图的深度优先搜索非常相似。
我不是数学家,也没有学过 C.S。因此,图论对我来说是一个新领域。尽管如此,我还是实现了一些(见下文)有效的方法(我怀疑效率低下)。

代码:

这是我的搜索和收益算法。如果你在下面的例子中运行它,你会看到它不止一次地访问了一些节点。因此,推测的效率低下。
关于输入的一句话。我写的规则基本上都是python类,有一个类属性depends。我因不使用inspect.getmro而受到批评-但这会使事情变得非常复杂,因为该类需要相互继承(See example here

def _yield_name_dep(rules_deps):
    global recursion_counter
    recursion_counter = recursion_counter +1 
    # yield all rules by their named and dependencies
    for rule, dep in rules_deps.items():
        if not dep:
            yield rule, dep
            continue
        else:
            yield rule, dep
            for ii in dep:
                i = getattr(rules, ii)
                instance = i()
                if instance.depends:
                    new_dep={str(instance): instance.depends}
                    for dep in _yield_name_dep(new_dep):
                        yield dep    
                else:
                    yield str(instance), instance.depends

好的,既然您已经盯着代码看,下面是一些您可以测试的输入:

demo_class_content ="""
class A(object):
    depends = ('B')
    def __str__(self):
        return self.__class__.__name__

class B(object):
    depends = ('C','F')
    def __str__(self):
        return self.__class__.__name__

class C(object):
    depends = ('D', 'E')
    def __str__(self):
        return self.__class__.__name__

class D(object):
    depends = None
    def __str__(self):
        return self.__class__.__name__   

class F(object):
    depends = ('E')
    def __str__(self):
        return self.__class__.__name__

class E(object):
    depends = None  
    def __str__(self):
        return self.__class__.__name__
"""       

with open('demo_classes.py', 'w') as clsdemo:
    clsdemo.write(demo_class_content)

import demo_classes as rules

rule_start={'A': ('B')}

def _yield_name_dep(rules_deps):
    # yield all rules by their named and dependencies
    for rule, dep in rules_deps.items():
        if not dep:
            yield rule, dep
            continue
        else:
            yield rule, dep
            for ii in dep:
                i = getattr(rules, ii)
                instance = i()
                if instance.depends:
                    new_dep={str(instance): instance.depends}
                    for dep in _yield_name_dep(new_dep):
                        yield dep    
                else:
                    yield str(instance), instance.depends

if __name__ == '__main__':
    # this is yielding nodes visited multiple times, 
    # list(_yield_name_dep(rule_start))
    # hence, my work around was to use set() ...
    rule_dependencies = list(set(_yield_name_dep(rule_start)))
    print rule_dependencies

问题:

  • 我尝试对我的工作进行分类,我认为我所做的与 DFS 类似。你真的可以这样分类吗?
  • 如何改进此功能以跳过访问过的节点,并且仍然使用生成器?

更新:

只是为了省去你运行代码的麻烦,上面函数的输出是:

>>> print list(_yield_name_dep(rule_wd))
[('A', 'B'), ('B', ('C', 'F')), ('C', ('D', 'E')), ('D', None), ('E', None), ('F', 'E'), ('E', None)]
>>> print list(set(_yield_name_dep(rule_wd)))
[('B', ('C', 'F')), ('E', None), ('D', None), ('F', 'E'), ('C', ('D', 'E')), ('A', 'B')]

虽然我想出了一个更好的解决方案,但上面的问题仍然存在。所以请随意批评我的解决方案:

visited = []
def _yield_name_dep_wvisited(rules_deps, visited):
    # yield all rules by their name and dependencies
    for rule, dep in rules_deps.items():
        if not dep and rule not in visited:
            yield rule, dep
            visited.append(rule)
            continue
        elif rule not in visited:
            yield rule, dep
            visited.append(rule)
            for ii in dep:
                i = getattr(grules, ii)
                instance = i()
                if instance.depends:
                    new_dep={str(instance): instance.depends}
                    for dep in _yield_name_dep_wvisited(new_dep, visited):
                        if dep not in visited:
                            yield dep    
                    
                elif str(instance) not in visited:
                    visited.append(str(instance))
                    yield str(instance), instance.depends

上面的输出是:

>>>list(_yield_name_dep_wvisited(rule_wd, visited))
[('A', 'B'), ('B', ('C', 'F')), ('C', ('D', 'E')), ('D', None), ('E', None), ('F', 'E')]

所以你现在可以看到节点 E 只被访问过一次。

【问题讨论】:

  • 为什么不将类本身存储在depends而不是类名中?
  • @Eric,首先是因为我不知道该怎么做。其次,这里的课程只是为了演示。该代码适用于配置文件中定义的一些属性,我想要一个自包含的示例。你能说明你的意思吗?
  • depends = (E, F),不带引号。请注意,您需要在文件中更早地定义所依赖的东西,而不是依赖它们的东西,但这对于可读性来说可能是一个好主意。
  • 我相信这个问题更适合codereview.SE 无论如何,如果你总是 使用depends 的元组,我相信你可以简化代码。我的意思是,拥有depends = Nonedepends = 'A'depends = ('A', 'B'),您必须明确处理这些情况。您可以简单地使用depends = ()depends = ('A',)depends = ('A', 'B') 并拥有更统一的代码。另一件事:instance = i(); if instance.depends: 因为depends 是一个 class 属性,所以您不必实例化该类。只需执行if i.depends:
  • 如果你总是使用元组,你可以完全删除if not dep ...,因为for 永远不会在没有依赖关系的情况下执行。正确知道您的代码仅在依赖项具有单字母名称时才有效,在其他情况下,depends = 'MultiLetterName'depends = ('A', 'B') 会出现问题,您必须检查 dep 是否为字符串。

标签: python algorithm generator depth-first-search


【解决方案1】:

根据 Gareth 和其他 * 用户的反馈,我得出了以下结论。它更清晰,也更笼统:

def _dfs(start_nodes, rules, visited):
    """
    Depth First Search
    start_nodes - Dictionary of Rule with dependencies (as Tuples):    

        start_nodes = {'A': ('B','C')}

    rules - Dictionary of Rules with dependencies (as Tuples):
    e.g.
    rules = {'A':('B','C'), 'B':('D','E'), 'C':('E','F'), 
             'D':(), 'E':(), 'F':()}
    The above rules describe the following DAG:

                    A
                   / \
                  B   C
                 / \ / \
                D   E   F
    usage:
    >>> rules = {'A':('B','C'), 'B':('D','E'), 'C':('E','F'), 
                 'D':(), 'E':(), 'F':()}
    >>> visited = []
    >>> list(_dfs({'A': ('B','C')}, rules, visited))
    [('A', ('B', 'C')), ('B', ('D', 'E')), ('D', ()), ('E', ()), 
    ('C', ('E', 'F')), ('F', ())]
    """

    for rule, dep in start_nodes.items():
        if rule not in visited:
            yield rule, dep
            visited.append(rule)
            for ii in dep:
                new_dep={ ii : rules[ii]}
                for dep in _dfs(new_dep, rules, visited):
                    if dep not in visited:
                        yield dep

【讨论】:

    【解决方案2】:

    这是另一种在不重复访问节点的情况下进行广度优先搜索的方法。

    import pylab
    import networkx as nx
    
    G = nx.DiGraph()
    G.add_nodes_from([x for x in 'ABCDEF'])
    G.nodes()
    

    返回 ['A', 'C', 'B', 'E', 'D', 'F']

    G.add_edge('A','B')
    G.add_edge('A','C')
    G.add_edge('B','D')
    G.add_edge('B','E')
    G.add_edge('C','E')
    G.add_edge('C','F')
    

    下面是如何在不重复节点的情况下遍历树。

    nx.traversal.dfs_successors(G)
    

    返回 {'A': ['C', 'B'], 'B': ['D'], 'C': ['E', 'F']} 然后你就可以画图了。

    nx.draw(G,node_size=1000)
    

    【讨论】:

    • 实际上,这是我最喜欢的解决方案。我的同事真的反对在项目中添加“另一个依赖项”。我认为最好避免重新发明*。虽然,当你重新发明*时,你会学到很多东西。
    • 您可以花时间学习很多关于其他人所做的事情或继续您的研究,这可能会帮助每个人学习新知识。那里有很多算法。小心你的时间。祝你好运。