• 怎么样,是不是很简单?

    下面我们来详细阐述一下细节,首先后序遍历和维护出栈顺序是一码事。也就是在递归的过程当中当我们遍历完了u这个节点所有连通的点之后,再把u加入序列。其实也就是u在递归出栈的时候才会被加入序列,那么序列当中存储的也就是每个点的出栈顺序。

    这里我用一小段代码演示一下,看完也就明白了。

    popped = [] # 存储出栈节点

    def dfs(u):
        for v in Graph[u]:
            dfs(v)
            popped.append(u)

    我们在访问完了所有的v之后再把u加入序列,这也就是后序遍历,和二叉树的后序遍历是类似的。

    反向图也很好理解,由于我们求解的范围是有向图,如果原图当中存在一条边从u指向v,那么反向图当中就会有一条边从v指向u。也就是把所有的边都调转反向。

    我们用上面的图举个例子,对于原图来说,它的出栈顺序我们用红色笔标出。

    也就是[6, 4, 2, 5, 3, 1],我们按照出栈顺序从大到小排序,也就是将它反序一下,得到[1, 3, 5, 2, 4, 6]。1是第一个,也就是最后一个出栈的,也意味着1是遍历的起点。

    我们将它反向之后可以得到:

    我们再次从1出发可以遍历到2,3, 4,说明{1, 2, 3, 4}是一个强连通分量。

    怎么样,整个过程是不是非常简单?

    我们将这段逻辑用代码实现,也并不会很复杂。

    N = 7
    graph, rgraph = [[] for _ in range(N)], [[] for _ in range(N)]
    used = [False for _ in range(N)]
    popped = []


    # 建图
    def add_edge(u, v):
        graph[u].append(v)
        rgraph[v].append(u)


    # 正向遍历
    def dfs(u):
        used[u] = True
        for v in graph[u]:
            if not used[v]:
                dfs(v)
        popped.append(u)


    # 反向遍历
    def rdfs(u, scc):
        used[u] = True
        scc.append(u)
        for v in rgraph[u]:
            if not used[v]:
                rdfs(v, scc)
                
    # 建图,测试数据         
    def build_graph():
        add_edge(13)
        add_edge(12)
        add_edge(24)
        add_edge(34)
        add_edge(35)
        add_edge(41)
        add_edge(46)
        add_edge(56)


    if __name__ == "__main__":
        build_graph()
        for i in range(1, N):
            if not used[i]:
                dfs(i)

        used = [False for _ in range(N)]
        # 将第一次dfs出栈顺序反向
        popped.reverse()
        for i in popped:
            if not used[i]:
                scc = []
                rdfs(i, scc)
                print(scc)

    思考

    算法讲完了,代码也写了,但是并没有结束,仍然有一个很大的疑惑没有解开。算法的原理很简单,很容易学会,但问题是为什么这样做就是正确的呢?这其中的原理是什么呢?我们似乎仍然没有弄得非常清楚。

    这里面的原理其实很简单,我们来思考一下,如果我们在正向dfs的时候,u点出现在了v点的后面,也就是u点后于v点出栈。有两种可能,一种可能是u点可以连通到v点,说明u是v的上游还有一种可能是u不能连通到v,说明图被分割成了多个部分。对于第二种情况我们先不考虑,因为这时候u和v一定不在一个连通分量里。对于第一种情况,u是v的上游,说明u可以连通到v。

    这时候,我们将图反向,如果我们从u还可以访问到v,那说明了什么?很明显,说明了在正向图当中v也有一条路径连向u,不然反向之后u怎么连通到v呢?所以,u和v显然是一个强连通分量当中的一个部分。我们再把这个结论推广,所有u可以访问到的,第一次遍历时在它之前出栈的点,都在一个强连通分量当中。

    如果你能理解了这一点,那么整个算法对你来说也就豁然开朗了,相信剩下的细节也都不足为虑了。

    今天的文章到这里就结束了,如果喜欢本文的话,请来一波素质三连,给我一点支持吧(关注、转发、点赞)。

    原文链接,求个关注

    - END -

    算法数据结构 | 三个步骤完成强连通分量分解的Kosaraju算法-LMLPHP

    09-17 00:53