zoukankan      html  css  js  c++  java
  • Kosaraju算法的分析和证明 转自http://blog.sina.com.cn/s/blog_4dff87120100r58c.html

    Kosaraju算法是求解有向图强连通分量(strong connected component)的三个著名算法之一,能在线性时间求解出一个图的强分量。用Sedgewick爷爷的话说,就是“现代算法设计的胜利”。

    什么是强连通分量?在这之前先定义一个强连通性(strong connectivity)的概念:有向图中,如果一个顶点s到t有一条路径,t到s也有一条路径,即s与t互相可达,那么我们说s与t是强连通的。那么在有向图中,由互相强连通的顶点构成的分量,称作强连通分量。

    首先说一些离散数学相关的结论,由强连通性的概念可以发现,这是一个等价关系。

    证明:

    一,按照有向图的约定,每个顶点都有到达自身的路径,即自环,即任意顶点s到s可达,满足自反性;

    二,如果s与t是强连通的,则s到t存在路径,t到s存在路径,显然t与s也是强连通的,满足对称性;

    三,如果r与s强连通,s与t强连通,则r与s互相可达,s与t互相可达,显然r与t也互相可达,满足传递性。

    因此,强连通关系可导出一个等价类,这就是强连通分量。进一步的利用这结论可以知道,两个强连通分量之间木有交集(这个结论很重要)。事实上,图论与离散数学中的关系有非常密切的……关系。

    在编程求解强连通分量时,通常做法是对顶点进行编号,拥有相同编号的顶点属于同一个强连通分量。在求解完之后,通过对编号的比较可以迅速判断两个顶点是否是强连通的。

     

    ------------------------------分割线-----------------------------------

     

    Kosaraju算法过程上并不复杂。要求解一个有向图的强连通分量,第一步:在该图的逆图上运行DFS,将顶点按照后序编号的顺序放入一个数组中(显然,这个过程作用在DAG上得到的就是一个拓扑排序);第二步:在原图上,按第一步得出的后序编号的逆序进行DFS。也就是说,在第二次DFS时,每次都挑选当前未访问的结点中具有最大后序编号的顶点作为DFS树的树根。

    Kosaraju算法的显著特征是,第一,引用了有向图的逆图;第二,需要对图进行两次DFS(一次在逆图上,一次在原图上)。而且这个算法依赖于一个事实:一个有向图的强连通分量与其逆图是一样的(即假如顶点任意顶点s与t属于原图中的一个强连通分量,那么在逆图中这两个顶点必定也属于同一个强连通分量,这个事实由强连通性的定义可证)。由于算法的时间取决于两次DFS,因此时间复杂度,对于稀疏图是O(V+E),对于稠密图是O(V²),可见这是一个线性算法。Kosaraju的结论是,在第二次DFS中,同一棵搜索树上的结点属于一个强连通分量。

    证明:假设顶点s与t属于第二次DFS森林(注意,第二次是在原图上搜索)的同一棵树,r是这棵树的根结点。那么有以下两个事实:一,原图中由r可达s,这蕴含在逆图中从s到r有一条路径;二,r在逆图中的后序编号大于s(r是树根,因此r的后序编号比树中所有的其他结点的都大)。现在要证明的是在逆图中从r到s也是可达的。

    好,两个事实结合起来:一,假设逆图中r到s不可达,且s到r存在路径,那么这条路径将使s的后序编号比r大,与事实一矛盾,排除;二,假设逆图中r到s存在路径,正是这条r到s的路径使得r有更大的后序编号,则r与s是强连通的,假设成立(看上去比较勉强,个人认为这应该是一个空证明)。显然,两个事实导出一个结论:逆图中,r与s互相可达。同理,r与t也互相可达,根据传递性,第二次DFS森林中同一棵树中的所有顶点构成一个强连通分量。

    另一方面,会不会一个强连通分量的所有顶点没有出现在第二次DFS森林的同一颗树中呢?答案是:不会。因为根据DFS的性质,如果r与s强连通,那么由r开始的DFS必定能搜到s。

    证毕。

    可见Kosaraju的方法能够找出有向图的强连通分量,那么为什么这个方法可行呢?或者如何实现呢?这正是Kosaraju算法最为精妙的地方,关键在于第二次DFS选取的顺序:在第一次DFS中,将顶点按照后序编号存放,第二次DFS就按照这个顺序的逆序进行搜索,这保证每次选取的根结点(刚才证明中的r结点)都具有未访问结点中最大的后序编号,则搜索中拓展的结点的后序编号都比根结点小,这样也就满足了事实二。

    补充:Kosaraju算法虽然是线性的,但是需要两次DFS,跟另外两个著名的求解强分量的算法相比,这是一个劣势。但是Kosaraju算法有个神奇之处在于:计算之后的强分量编号的顺序,刚好是该有向图K(D)(kernel DAG, 核心DAG)的一个拓扑排序!因此Kosaraju算法同时提供了一个计算有向图K(D)拓扑排序的线性算法。这个结果在一些应用中非常重要。

    最后附上我的实现~就一目了然啦~

     

    ---------------------------分割线again--------------------------------

     

    // Kosaraju算法邻接矩阵实现

     

    static int cnt, cntR, pre[MAXV], postR[MAXV];

    int Kosaraju(Graph G) {

      int v;

      // 初始化全局变量

      cnt = cntR = 0;

      for (v = 0; v < G->V; ++v)

        pre[v] = postR[v] = -1;

      // 第一次DFS,计算逆图的后序编号

      for (v = 0; v < G->V; ++v)

        if (pre[v] == -1)

          dfsPostR(G, v);

      cnt = 0;

      for (v = 0; v < G->V; ++v)

        G->sc[v] = -1;  // G->sv[v]表示顶点v的强连通分量编号

      // 第二次DFS,强连通分量编号

      for (v = G->V - 1; v >= 0; --v) {

        // 注意搜索的顶点顺序是逆图后序编号的逆序

        if (G->sc[postR[v]] == -1) {

          dfsSC(G, postR[v]);

          ++cnt;  // 对一棵树编号之后计数器值加1

        }

      }

      return cnt;  // 返回强连通分量的个数

    }

     

    void dfsPostR(Graph G, int v) {

      // 对逆图后序编号

      int t;

      pre[v] = cnt++;

      for (t = 0; t < G->V; ++t)

        if (G->adj[t][v] == 1)  // 注意!!!邻接矩阵引用逆图,因此是G->adj[t][v]

          if (pre[t] == -1)

            dfsPostR(G, t);

      postR[cntR++] = v;  // 后序编号,注意是计数器做数组下标

    }

     

    void dfsSC(Graph G, int v) {

      int t;

      G->sc[v] = cnt; // 计数器作为编号

      for (t = 0; t < G->V; ++t)

        if (G->adj[v][t] == 1)

          if (G->sc[t] == -1)

            dfsSC(G, t);

    }

     

    int GraphSC(Graph G, int s, int t) {

      // 比较顶点的强连通分量编号即可判断是否强连通

      return G->sc[s] == G->sc[t];

    }

  • 相关阅读:
    C++中static修饰的静态成员函数、静态数据成员
    C++友元函数、友元类
    C++异常处理
    运行时类型识别RTTI
    AD转换
    敏捷模式下的测试用例该如何存在?
    使用Postman轻松实现接口数据关联
    接口测试Mock利器-moco runner
    python测开平台使用dockerfile构建镜像
    MySQL – 用SHOW STATUS 查看MySQL服务器状态
  • 原文地址:https://www.cnblogs.com/vermouth/p/3710185.html
Copyright © 2011-2022 走看看