networkx源码解析-无权图最短路径

    在项目中如果我们不关注边的定量关系的话就可以用无权图,比如我们的图谱中保存的是朋友关系,我们一般只是关注两者之间是否是朋友而不太关注它们之间的定量关系-比如亲疏。我们可以假设无权图的边的权重都为1。

    相较于有权图,计算无权图的最短路径要简单一些且算法复杂度也小很多,因为无权图的最短路径基本上只需要进行一次广度优先搜索就可以了。

    下面来分析一下networkx求无权图最短路径的源码,我当前使用的networkx版本是2.1,以下面的图为例

networkx源码解析-无权图最短路径

import networkx as nx

g = nx.DiGraph()
g.add_edges_from([(1, 2), (1, 3), (2, 4), (3, 5), (4, 1), (4, 5)]) 
print(list(nx.all_shortest_paths(g, 1, 4)))

上面的代码求1和4之前的所有最短路径,输出:

[[1, 2,4]]

在IDE中按住ctrl点击all_shortest_paths定位到源文件:

C:\ProgramData\Anaconda2\Lib\site-packages\networkx\algorithms\shortest_paths\generic.py

def all_shortest_paths(G, source, target, weight=None):
    if weight is not None:
        pred, dist = nx.dijkstra_predecessor_and_distance(G, source,
                                                          weight=weight)
    else:
        pred = nx.predecessor(G, source)

    if source not in G:
        raise nx.NodeNotFound('Source {} is not in G'.format(source))

    if target not in pred:
        raise nx.NetworkXNoPath()

    stack = [[target, 0]]
    top = 0
    while top >= 0:
        node, i = stack[top]
        if node == source:
            yield [p for p, n in reversed(stack[:top + 1])]
        if len(pred[node]) > i:
            top += 1
            if top == len(stack):
                stack.append([pred[node][i], 0])
            else:
                stack[top] = [pred[node][i], 0]
        else:
            stack[top - 1][1] += 1
            top -= 1

 大致一看代码分两部分,首先调用nx.predecessor()函数生成前驱关系,然后再利用栈回溯遍历这个前驱关系形成多条路径,当发现遍历到的结点是源点(source)时表示找到一条路径yield返回。


下面是详细分析:

    首先判断传进来的图是有权图还是无权图,分情况做不同处理。我们今天只看无权图后面再分析有权图,首先调用pred =nx.predecessor(G, source),这个函数的作用是返回图G以source为起点进行广度优先遍(这篇文章讲广度优先遍历)历形成的前驱关系,所谓前驱关系就是一个字典它指示了一个结点的父结点有哪些,key是结点value是所有父结点的列表,比如上面的图的前驱字典就是:

{1: [],2: [1], 3: [1], 4: [2, 3]}

表示1是起点因此没有前驱,2的前驱是1,4的前驱是2和3因为有两条边指向它

    生成了前驱关系,事情就完成一大半了,剩下的事情就只是从目标结点开始顺着前驱关系一直往前走直到起点,形成的所有路径就是所有最短路径了。因为最短路径可能有多条,所以这里需要使用回溯法探索,networkx首先定义了一个栈(其实是一个列表因为python中没有直接提供栈的数据结构)用来顺序的保存当前已探索的结点,栈中的所有元素形成一条路径,栈中的每个元素包含两个信息:结点以及已经遍历了它的第几个父结点的索引,这个索引用来标记已经走过的路下次不要再走重路径了,当索引等于那个结点的前驱结点的总个数时表示已经探索完了那个结点的所有路径,继续回退,直到回退到起点此时栈为空(top<0)

    在实际项目中,在遍历的时候我们一般会加上一些条件,比如如果遍历到指定的层数时仍然没有找到目标结点,我们会返回没有找到。由于关系的强度往往随着路径长度呈指数级下降所以太长的路径是没有太大意义的。nx.predecessor()函数就带有一个参数cutoff表示遍历的最大深度。