【问题标题】:How to calculate overall distances from lowest root(s) of a directed graph with networkx如何使用networkx计算有向图最低根的总距离
【发布时间】:2021-06-09 13:42:59
【问题描述】:

如果你看一下这个 DAG(有向无环图):

我想创建一个字典,它映射从最低节点到所有其他节点的距离,这类似于渲染图中每个节点从底部到底部的 x 位置(高度)。 对于给定的图表,它将是:

distance_nodes_map: {
  0: {'base-zero', 'base-one'}, 
  1: {'low-b', 'low-a', 'low-c'}, 
  3: {'high-x', 'high-z', 'high-y'}, 
  2: {'mid-r', 'mid-q', 'mid-p'}, 
  4: {'super'}
}

我编写了一个适用于上述图表的算法,但后来我测试了另一个图表,但它不再起作用了。我尝试了一些算法和函数,例如最短路径或 descendants_at_distance,但我认为它们作为计算距离的输入并没有真正的帮助。

例如,我的算法不适用于此图:

https://gist.github.com/timaschew/3b08a07243fa6f43773014ef5e705c96

这是要点,其中包含:

  • 一个 python 脚本,它读取 YAML 文件、依赖项/图结构并生成带有渲染美人鱼图的 HTML(我已经删除了以错误方式计算距离的算法)
  • 此处显示的两个图表均以 YAML 文件形式显示

【问题讨论】:

    标签: python graph networkx


    【解决方案1】:

    您正在寻找一种绘制分层图的算法。有许多不同的算法,您应该选择最适合您需求的算法(例如,查看 Gansner 等人的以下论文 A Technique for Drawing Directed Graphs)。

    其中许多算法已经在Graphviz(一个非常著名且功能强大的图形可视化软件)中实现。安装后,计算您正在寻找的结果非常简单(G 是您使用 networkx.DiGraph 构建的有向无环图):

    from networkx.drawing.nx_agraph import graphviz_layout
    
    def get_distance_nodes_map(G):
        pos = graphviz_layout(G, prog='dot')
        coor = sorted({y for k, (x, y) in pos.items()})
        kmap = dict(zip(coor, range(len(coor))))
        distance_nodes_map = {level: set() for level in kmap.values()}
        for k, (x, y) in pos.items():
            distance_nodes_map[kmap[y]].add(k)
        return distance_nodes_map
    

    以下是使用您提供的数据的几个示例:

    >>> from networkx import DiGraph
    >>> from pprint import PrettyPrinter
    >>> pp = PrettyPrinter()
    >>> G1 = DiGraph()
    >>> G1.add_edges_from([('super', 'high-x'), ('high-x', 'mid-p'),
    ...                    ('mid-p', 'low-b'), ('mid-p', 'low-c'),
    ...                    ('low-c', 'base-zero'), ('low-c', 'base-one'),
    ...                    ('high-y', 'mid-p'), ('high-y', 'base-zero'),
    ...                    ('high-z', 'base-one'), ('high-z', 'mid-r'),
    ...                    ('high-z', 'mid-q'), ('mid-q', 'low-a'),
    ...                    ('low-a', 'base-one')])
    >>> pp.pprint(get_distance_nodes_map(G1))
    {0: {'base-one', 'base-zero'},
     1: {'low-a', 'low-b', 'low-c'},
     2: {'mid-p', 'mid-r', 'mid-q'},
     3: {'high-y', 'high-x', 'high-z'},
     4: {'super'}}
    >>> G2 = DiGraph()
    >>> G2.add_edges_from([('n10', 'n11'), ('n11', 'n12'), ('n12', 'n13'),
    ...                    ('n13', 'n14'), ('n20', 'n14'), ('n20', 'n21'),
    ...                    ('n21', 'n22'), ('n22', 'n23'), ('n30', 'n23'),
    ...                    ('n30', 'n31'), ('n31', 'n32')])
    >>> pp.pprint(get_distance_nodes_map(G2))
    {0: {'n32'},
     1: {'n31', 'n23'},
     2: {'n30', 'n22'},
     3: {'n21', 'n14'},
     4: {'n13', 'n20'},
     5: {'n12'},
     6: {'n11'},
     7: {'n10'}}
    

    【讨论】:

    • 太棒了,代码比“手工”少得多。谢谢!
    【解决方案2】:

    未经测试的伪代码,因为我的午休时间快结束了:

    你有一棵多根树,有一个选定的主根。

    1. 对于每个根,创建一个由该根的所有可达节点组成的子图。

    2. 从主根(根 a)开始,计算对应子图 A 中所有节点到根的距离/最短路径长度。

    3. 找到所有与主子图共享至少一个节点的子图,并选择与主根距离最小的节点(节点x)的子图(子图B)。

    4. 计算子图 B 中所有节点到根 b 的距离。添加距离 d(node x, root a)。减去距离 d(node x, root b)。

    5. 创建子图 A 和 B 的并集。重复步骤 3-5,直到没有根。

    6. 减去最大距离并反转符号,使得主根具有最大的距离/阶值。

    编辑:

    我的伪代码有效 (*)。我责怪用户错误。 ;-)

    #!/usr/bin/env python
    """
    https://stackoverflow.com/q/66584661/
    """
    import numpy as np
    import matplotlib.pyplot as plt
    import networkx as nx
    
    
    def hierarchical_layout(graph):
    
        longest_path = nx.algorithms.dag.dag_longest_path(graph)
        principal_root = longest_path[0]
    
        roots = [node for node, degree in list(graph.in_degree) if degree==0]
        subgraphs = {root : create_subgraph(graph, root) for root in roots}
    
        # Starting with the principal root (root a), compute the
        # longest path length to the root for all nodes in the
        # corresponding subgraph A.
        node_to_level = single_source_longest_dag_path_length(subgraphs[principal_root], principal_root)
    
        explored = subgraphs[principal_root]
        del subgraphs[principal_root]
    
        while len(explored) < len(graph):
    
            # Find all subgraphs that share at least one node with the
            # principal subgraph, and select the subgraph (subgraph B) that
            # has the node (node x) with the smallest distance to the
            # principal root.
            minimum_cost = np.inf
            minimum_cost_node = None
            minimum_cost_root = None
            for root, subgraph in subgraphs.items():
                for node in subgraph.nodes:
                    if node in node_to_level:
                        if node_to_level[node] < minimum_cost:
                            minimum_cost = node_to_level[node]
                            minimum_cost_node = node
                            minimum_cost_root = root
    
            assert minimum_cost_node, "Could not find a connected subgraph."
    
            # Compute the distance to the root b for all nodes in subgraph
            # B. Add the distance d(node x, root a). Subtract the distance
            # d(node x, root b).
            path_lengths = [len(path) for path in nx.all_simple_paths(subgraphs[minimum_cost_root], minimum_cost_root, minimum_cost_node)]
            offset = np.max(path_lengths) - 1
            for node, distance in single_source_longest_dag_path_length(subgraphs[minimum_cost_root], minimum_cost_root).items():
                if not node in node_to_level:
                    node_to_level[node] = distance + minimum_cost - offset
    
            # Create the union of subgraph A and B.
            explored = nx.compose(explored, subgraphs[minimum_cost_root])
            del subgraphs[minimum_cost_root]
    
        return node_to_level
    
    
    def create_subgraph(G, node):
        # https://stackoverflow.com/a/45678930/2912349
        nodes = nx.single_source_shortest_path(G,node).keys()
        return G.subgraph(nodes)
    
    
    def single_source_longest_dag_path_length(graph, s):
        # from AlaskaJoslin's comment to https://stackoverflow.com/a/60978007/2912349
        dist = dict.fromkeys(graph.nodes, -float('inf'))
        dist[s] = 0
        topo_order = nx.topological_sort(graph)
        for n in topo_order:
            for s in graph.successors(n):
                if dist[s] < dist[n] + 1:
                    dist[s] = dist[n] + 1
        return dist
    
    
    if __name__ == '__main__':
    
        # edge_list = [
        #     ("n10", "n11"),
        #     ("n11", "n12"),
        #     ("n12", "n13"),
        #     ("n13", "n14"),
        #     ("n20", "n21"),
        #     ("n20", "n14"),
        #     ("n21", "n22"),
        #     ("n22", "n23"),
        #     ("n30", "n23"),
        #     ("n30", "n31"),
        #     ("n31", "n32"),
        # ]
    
        edge_list = [
            ("low-a", "base-one"),
            ("low-c", "base-zero"),
            ("low-c", "base-one"),
            ("mid-p", "low-b"),
            ("mid-p", "low-c"),
            ("mid-q", "low-a"),
            ("high-x", "mid-p"),
            ("high-y", "mid-p"),
            ("high-y", "base-zero"),
            ("high-z", "mid-q"),
            ("high-z", "mid-r"),
            ("high-z", "base-one"),
            ("super", "high-x"),
        ]
    
        graph = nx.DiGraph()
        graph.add_edges_from(edge_list)
    
        node_to_level = hierarchical_layout(graph)
    
        # reverse output format
        distance_nodes_map = dict()
        max_distance = np.max(list(node_to_level.values()))
        for node, distance in node_to_level.items():
            reversed_distance = max_distance - distance
            if reversed_distance in distance_nodes_map:
                distance_nodes_map[reversed_distance].add(node)
            else:
                distance_nodes_map[reversed_distance] = set([node])
    
        # print(distance_nodes_map)
        for ii, nodes in sorted(distance_nodes_map.items())[::-1]:
            print(f"{ii} : {nodes}")
    

    产量:

        # 4 : {'super'}
        # 3 : {'high-x', 'high-y', 'high-z'}
        # 2 : {'mid-p', 'mid-r', 'mid-q'}
        # 1 : {'low-a', 'low-b', 'low-c'}
        # 0 : {'base-one', 'base-zero'}
    

    (*) "减去距离 d(node x, root b)" 自然暗示了节点 x 和 root b 之间的最长路径长度。很明显。

    【讨论】:

    • 感谢您的回答。子图的想法非常好,我试图用它和 nx.dag_longest_path_length 的使用来修改我现有的算法。但是第二张图的结果还可以,但是对于第一张图它已经坏了。我不确定如何实现您的伪代码,所以如果您有兴趣,我已经开始为真正的实现提供赏金;)
    • @timaschew 用工作代码更新了我的答案。
    • 谢谢,看起来很棒!有一些缺失的部分。在我的图表中确实有一个主根,但这是巧合。正如赏金描述中所写,它必须适用于所有 DAG。但是即使对于这两个图,仍然存在一个问题:如何找出主根?我试图选择一个随机的根,但结果是错误的。一般来说,我们是否需要遍历所有根并将输出合并在一起,或者解决方案是什么?
    • @timaschew 选择到叶子路径长度最长的根(in-degree=0)(out-degree=0)。
    • @timaschew 添加了计算主根的方法。
    猜你喜欢
    • 2013-09-10
    • 2019-03-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多