【问题标题】:All path *lengths* from source to target in Directed Acyclic Graph有向无环图中从源到目标的所有路径*长度*
【发布时间】:2020-10-15 05:14:15
【问题描述】:

我有一个邻接矩阵形状的图形 (adj_mat.shape = (4000, 4000))。我当前的问题涉及查找从源 (row = 0 ) 遍历到目标 (col = trans_mat.shape[0] -1) 的路径长度列表(节点序列并不那么重要)。

对查找路径序列感兴趣;我只对传播路径长度感兴趣。因此,这与查找 所有简单路径 不同 - 这太慢了(即,查找从源到目标的所有路径;然后对每条路径进行评分)。有没有一种高效的方法可以快速做到这一点?


建议使用 DFS as one possible strategy (noted here)。我当前的实现(如下)根本不是最佳的:

# create graph
G = nx.from_numpy_matrix(adj_mat, create_using=nx.DiGraph())

# initialize nodes
for node in G.nodes:
    G.nodes[node]['cprob'] = []

# set starting node value
G.nodes[0]['cprob'] = [0]

def propagate_prob(G, node):

    # find incoming edges to node
    predecessors = list(G.predecessors(node))
    curr_node_arr = []        

    for prev_node in predecessors:
        # get incoming edge weight
        edge_weight = G.get_edge_data(prev_node, node)['weight']

        # get predecessor node value
        if len(G.nodes[prev_node]['cprob']) == 0:                
            G.nodes[prev_node]['cprob'] = propagate_prob(G, prev_node)            
        prev_node_arr = G.nodes[prev_node]['cprob']   

        # add incoming edge weight to prev_node arr
        curr_node_arr = np.concatenate([curr_node_arr, np.array(edge_weight) + np.array(prev_node_arr)])

    # update current node array
    G.nodes[node]['cprob'] = curr_node_arr
    return G.nodes[node]['cprob']

# calculate all path lengths from source to sink 
part_func = propagate_prob(G, 4000)

【问题讨论】:

  • 我不认为你可以在不找到路径的情况下找到路径长度。
  • 也许我理解错了。我可以想象在每个节点传播传入边权重的列表。连接边权重 + 所有前面的传入边权重描绘了所有可能的路径长度。
  • 但同样的也构建了所有路径。
  • 我根据您关于查找路径与查找路径长度的语义的问题编辑了我的问题。但是在不记录路径序列的情况下找到路径长度真的找到了路径吗?我不太确定。例如,在 HMM 中,前向算法不需要枚举所有可能发出感兴趣序列的路径;它只是传播从先前状态传播的概率。

标签: graph networkx igraph depth-first-search breadth-first-search


【解决方案1】:

我没有手动的大例子(例如>300个节点),但我找到了一个非递归解决方案:

import networkx as nx

g = nx.DiGraph()

nx.add_path(g, range(7))

g.add_edge(0, 3)
g.add_edge(0, 5)
g.add_edge(1, 4)
g.add_edge(3, 6)

# first step retrieve topological sorting
sorted_nodes = nx.algorithms.topological_sort(g)

start = 0
target = 6

path_lengths = {start: [0]}

for node in sorted_nodes:
    if node == target:
        print(path_lengths[node])
        break

    if node not in path_lengths or g.out_degree(node) == 0:
        continue
    new_path_length = path_lengths[node]
    new_path_length = [i + 1 for i in new_path_length]
    for successor in g.successors(node):
        if successor in path_lengths:
            path_lengths[successor].extend(new_path_length)
        else:
            path_lengths[successor] = new_path_length.copy()

    if node != target:
        del path_lengths[node]

输出:[2, 4, 2, 4, 4, 6]

如果您只对不同长度的路径数量感兴趣,例如{2:2, 4:3, 6:1} 对于上述示例,您甚至可以将列表缩减为 dicts。

背景

一些解释我在做什么(我希望也适用于更大的例子)。第一步是检索拓扑排序。为什么?然后我知道边缘流向哪个“方向”,我可以简单地按该顺序处理节点,而不会像递归变体那样“丢失任何边缘”或任何“回溯”。之后,我使用包含当前路径长度 ([0]) 的列表初始化起始节点。该列表被复制到所有后继者,同时更新路径长度(所有元素+1)。目标是在每次迭代中计算从起始节点到所有处理节点的路径长度并将其存储在字典path_lengths 中。到达target-node 后循环停止。

【讨论】:

  • 是的,拓扑顺序很重要——你完全正确。将其排序需要 O(V+E) 时间。我将在更大的图表上试一试以测试性能。
  • 我试了一下,但它在 200 个节点后标记。我怀疑这与每个 networkx 调用的开销有关。我希望你不介意,我赞成你的回答,但我会留下这个问题。
  • 你的意思是flags需要太多内存或者单个节点的处理时间太长?您是否尝试过 dict 替代方案(如果这符合您的要求)?
  • 是的,后者 - 即使没有递归,Networkx 的单个节点计算也需要很长时间。我认为也许使用 LightGraphs(在 Julia 中?)它可能会更快。我会试试你的字典方法,谢谢你的建议。
【解决方案2】:

使用igraph,我可以在大约 1 秒内计算多达 300 个节点。我还发现访问邻接矩阵本身(而不是调用igraph 的函数来检索边/顶点)也可以节省时间。两个关键瓶颈是 1)以有效的方式附加一个长列表(同时还保留内存) 2)找到一种并行化的方法。这次以指数方式增长超过约 300 个节点,我很想看看是否有人有更快的解决方案(同时也适合内存)。

import igraph

# create graph from adjacency matrix
G = igraph.Graph.Adjacency((trans_mat_pad > 0).tolist())

# add edge weights
G.es['weight'] = trans_mat_pad[trans_mat_pad.nonzero()]

# initialize nodes
for node in range(trans_mat_pad.shape[0]):
    G.vs[node]['cprob'] = []

# set starting node value
G.vs[0]['cprob'] = [0]

def propagate_prob(G, node, trans_mat_pad):

    # find incoming edges to node
    predecessors = trans_mat_pad[:, node].nonzero()[0] # G.get_adjlist(mode='IN')[node]
    curr_node_arr = []        

    for prev_node in predecessors:
        # get incoming edge weight
        edge_weight = trans_mat_pad[prev_node, node] # G.es[prev_node]['weight']

        # get predecessor node value
        if len(G.vs[prev_node]['cprob']) == 0:
            curr_node_arr = np.concatenate([curr_node_arr, np.array(edge_weight) + propagate_prob(G, prev_node, trans_mat_pad)])
        else: 
            curr_node_arr = np.concatenate([curr_node_arr, np.array(edge_weight) + np.array(G.vs[prev_node]['cprob'])])
    ## NB: If memory constraint, uncomment below
    # set max size
    # if len(curr_node_arr) > 100:
    #     curr_node_arr = np.sort(curr_node_arr)[:100]
    
    # update current node array
    G.vs[node]['cprob'] = curr_node_arr
    return G.vs[node]['cprob']

# calculate path lengths
path_len = propagate_prob(G, trans_mat_pad.shape[0]-1, trans_mat_pad)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-03-10
    • 2013-10-25
    • 2019-02-15
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多