强连通分支算法--Kosaraju算法

来源:互联网 发布:mac出现竖条怎么办 编辑:程序博客网 时间:2024/05/17 03:15

强连通分支算法

本节内容将详细讨论有向图的强连通分支算法(strongly connected component),该算法是图深度优先搜索算法的另一重要应用。强分支算法可以将一个大图分解成多个连通分支,某些有向图算法可以分别在各个联通分支上独立运行,最后再根据分支之间的关系将所有的解组合起来。

在无向图中,如果顶点s到t有一条路径,则可以知道从t到s也有一条路径;在有向无环图中个,如果顶点s到t有一条有向路径,则可以知道从t到s必定没有一条有向路径;对于一般有向图,如果顶点s到t有一条有向路径,但是无法确定从t到s是否有一条有向路径。可以借助强连通分支来研究一般有向图中顶点之间的互达性。

有向图G=(V, E)的一个强连通分支就是一个最大的顶点子集C,对于C中每对顶点(s, t),有s和t是强连通的,并且t和 s也是强连通的,即顶点s和t是互达的。图中给出了强连通分支的例子。我们将分别讨论3种有向图中寻找强连通分支的算法。

3种算法分别为Kosaraju算法、Tarjan算法和Gabow算法,它们都可以在线性时间内找到图的强连通分支。

Kosaraju算法

Kosaraju算法的解释和实现都比较简单,为了找到强连通分支,首先对图G运行DFS,计算出各顶点完成搜索的时间f;然后计算图的逆图GT,对逆图也进行DFS搜索,但是这里搜索时顶点的访问次序不是按照顶点标号的大小,而是按照各顶点f值由大到小的顺序;逆图DFS所得到的森林即对应连通区域。具体流程如图(1~4)。

上面我们提及原图G的逆图GT,其定义为GT=(V, ET),ET={(u, v):(v, u)∈E}}。也就是说GT是由G中的边反向所组成的,通常也称之为图G的转置。在这里值得一提的是,逆图GT和原图G有着完全相同的连通分支,也就说,如果顶点s和t在G中是互达的,当且仅当s和t在GT中也是互达的。

 

 

 

根据上面对Kosaraju算法的讨论,其实现如下:

 

复制代码
    /**
     * 算法1:Kosaraju,算法步骤:
     * 1. 对原始图G进行DFS,获得各节点的遍历次序ord[];
     * 2. 创建原始图的反向图GT;
     * 3. 按照ord[]相反的顺序访问GT中每个节点;
     * 
@param G    原图
     * 
@return    函数最终返回一个二维单链表slk,单链表
     * 每个节点又是一个单链表, 每个节点处的单链表表示
     * 一个联通区域;slk的长度代表了图中联通区域的个数。
     
*/
    public static SingleLink2 Kosaraju(GraphLnk G){
        SingleLink2 slk = new SingleLink2();
        int ord[] = new int[G.get_nv()];
        // 对原图进行深度优先搜索
        GraphSearch.DFS(G);
        // 拷贝图G的深度优先遍历时每个节点的离开时间
        for(int i = 0; i < GraphSearch.f.length; i++){
            ord[i] = GraphSearch.f[i];
            System.out.print(GraphSearch.parent[i] + " || ");
        }
        System.out.println();
        // 构造G的反向图GT
        GraphLnk GT = Utilities.reverseGraph(G);
        /* 用针对Kosaraju算法而设计DFS算法KosarajuDFS函数
         * 该函数按照ord的逆向顺序访问每个节点,
         * 并向slk中添加新的链表元素;
*/
        GraphSearch.KosarajuDFS(GT, ord, slk);
        //打印所有的联通区域
        for(slk.goFirst(); slk.getCurrVal()!= null; slk.next()){
            //获取一个链表元素项,即一个联通区域
            GNodeSingleLink comp_i = 
                    (GNodeSingleLink)(slk.getCurrVal().elem);
            //打印这个联通区域的每个图节点
            for(comp_i.goFirst(); 
                comp_i.getCurrVal() != null; comp_i.next()){
                System.out.print(comp_i.getCurrVal().elem + "\t");
            }
            System.out.println();
        }
        //返回联通区域链表
        return slk;
    }
复制代码

 

算法首先对原图进行DFS搜索,并记录每个顶点的搜索离开时间ord;然后调用Utilities类中的求逆图函数reverseGraph,求得原图的逆图GT;最后调用GraphSearch类的递归函数KosarajuDFS,该函数按照各顶点的ord时间对图进行深度优先搜索。函数最终返回一个二维单链表slk,单链表每个节点的元素项也是一个单链表,每个节点处的单链表表示一个联通区域,如图,slk的长度代表了图中联通区域的个数。之所以使用这样的数据结构作为函数的结果,其原因是在函数返回之前无法知道图中共有多少个强连通分支,也不知道每个分支的顶点的个数,二维链表自然成为最优的选择。其余两个算法的返回结果也采用这种形式的链表输出结果。

 

 

 

 

 

图 Kosaraju算法返回的二维链表形式结果

reverseGraph函数返回的逆图是新创建的图,其每条边都与原图边的方向相反。原图中一个顶点对应的链表中的相邻顶点是按照标号由小到大的顺序排列的,这样做能提高相关算法的效率(例如isEdge(int, int)函数)。这里在计算逆图时这个条件也必须满足。该静态函数的实现如下:

 

复制代码
    /**
         * 创建一个与入参G每条边方向相反的图,即逆图。
         * 
@param G    原始图
         * 
@return 逆图
         
*/
        public static GraphLnk reverseGraph(GraphLnk G){
            GraphLnk GT = new GraphLnk(G.get_nv());
            for(int i = 0; i < G.get_nv(); i++){
                //GT每条边的方向与G的方向相反
                for(Edge w = G.firstEdge(i); G.isEdge(w); 
                    w = G.nextEdge(w)) {
                     GT.setEdgeWt(w.get_v2(), w.get_v1(),
                                 G.getEdgeWt(w));
                }
            }
            return GT;
        }
复制代码

 

函数中对顶点按照标号由小到大进行遍历,对顶点i,再遍历其对应链表中的顶点i0, …, in,并向逆图中添加边(i0, i), …, (in, i),最终得到原图的逆图GT。可以发现,函数中调用setEdgeWt函数来向GT中添加边。细心的读者可能还记得,若待修改权值的边不存在时,函数setEdgeWt充当添加边的功能,并且能保证相邻的所有顶点的标号在链表中按由小到大的顺序排列。

Kosaraju实现中关键的一步是调用KosarajuDFS函数对逆图GT进行递归深度优先搜索。函数的另外两个形参为原图DFS所得的各顶点搜索结束时间ord,和保存连通分支结果的链表slk。KosarajuDFS的实现如下:

复制代码
    /**
     * 将根据逆图GT和每个节点在原图G深度遍历时的离开时间数组,
     * 生成链表slk表示的连通分支
     * 本函数不改变图的连接关系,只是节点的访问次序有所调整.
     * 
@param GT    逆图
     * 
@param ord    原图DFS各顶点遍历离开时间数组
     * 
@param slk    连通分支存放在链表中
     
*/
    public static void KosarajuDFS(GraphLnk GT, int ord[], 
                                    SingleLink2 slk){
        /* 根据ord数组计算new_order数组,新的访问顺序为:
         * 第i次访问的节点为原图上的第new_order[i]个节点 
*/
        int new_order[] = new int[ord.length];
        //调用函数newordermap改变数组的次序
        newordermap(ord, new_order);
        int n = GT.get_nv();
        // 这里只需要记录颜色,其它信息不重要了
        color  = new COLOR[n];
        // 颜色初始化为白色
        for(int i = 0; i < n; i++) 
            color[i] = COLOR.WHITE;
        // 为找到图中所有的联通区域,循环迭代:
        for(int i = 0; i < new_order.length; i++){
            // 第i次访问的节点为原图上的第new_order[i]个节点
            int  j= new_order[i];
            if(color[j] != COLOR.WHITE) 
                continue;
            // 创建一个图节点链表,表示一个联通区域
            GNodeSingleLink gnsk = new GNodeSingleLink();
            //调用递归函数,以j为起点深度搜索该图
            KosarajuDFSVISIT(GT, ord, j, gnsk);
            /* 将联通区的节点形成的链表添加到联通区域链表中,
             * 这里使用的实际上是二维链表
*/
            slk.append(new ElemItem<GNodeSingleLink>(gnsk));
        }
    }
复制代码

 

函数首先根据ord数组确定GT中各顶点的访问次序,方法是创建大小与ord相等的数组new_ord,并调用newordermap函数给new_ord各元素赋值。new_ord[i]的意义是:原图上第i个节点在逆图的访问次序为new_ord[i]。

例如顶点在原图中DFS遍历离开时间ord为:

11

0)16

1)10

2)15

3)9

4)14

5)8

6)13

7)7

8)20

9)19

则各顶点在逆图中访问先后次序为:

8, 9, 0, 2, 4, 6, 1, 3, 5, 7,

newordermap函数实现如下:

 

 

 

复制代码
    /**
     * 根据原图各顶点DFS得到的遍历结束时间,获取节点新
     * 的访问次序.在函数返回时,new_ord[i]的意义是:
     * 原图上第i个节点在逆图的访问次序为new_ord[i];
     * 其效果是:后访问的节点在新的访问次序中先访问.
     * 
@param ord        原图各顶点DFS遍历结束时间
     * 
@param new_ord    节点在逆图的访问次序
     
*/
    public static void newordermap(int[] ord, int[] new_ord){
        // 为防止原ord数组被破坏,对其深拷贝
        int ord_temp[] = new int[ord.length];
        for(int i = 0; i < ord.length; i++) 
            ord_temp[i] = ord[i];
        int max, max_idx;
        // 在ord_temp中寻找n次最大的值,并将其数组下标放
        
// 置到new_ord中;然后将最大值赋值为-1
        for(int i = 0; i < ord.length; i++){
            max_idx = 0; 
            max = ord_temp[max_idx];
            for(int j = 0; j < ord.length; j++){
                if(ord_temp[j] == -1) 
                    continue;
                if(ord_temp[j] > max){
                    max = ord_temp[j];
                    max_idx = j;
                }
            }
            new_ord[i] = max_idx;
            ord_temp[max_idx] = -1;
        }
    }
复制代码

 

确定各顶点的访问次序后,按照new_ord中顶点的顺序调用递归函数KosarajuDFSVISIT对逆图进行深度优先搜索。每次调用前都创建一个新的链表作为slk链表的节点,对应一个新的强连通分支。

 

 

复制代码
    /**
     * 递归函数,起点为u深度搜索图G,节点递归地调用该函数时,节点的访问顺序
     * 由ord决定,对节点u与之相连且标记为白色的的节点v1,v2,...
     * 先访问ord[vi]最大的节点vi.当没有与节点u相连的节点或者与u相连的所有
     * 节点都不是白色的了,此时获得一个联通区域,函数返回
    
*/
    public static void KosarajuDFSVISIT(GraphLnk G, int ord[], 
                         int u, GNodeSingleLink slk){
        //访问该节点,将其颜色标记为灰色
        color[u] = COLOR.GRAY;
        //将该节点u添加到当前的联通区域中
        slk.append(new ElemItem<Integer>(u));
        //首先统计与该节点相连的、颜色为白色的节点的个数
        int cnt = 0;
        for(Edge w = G.firstEdge(u); 
                G.isEdge(w); w = G.nextEdge(w)){
            if(color[w.get_v2()] == COLOR.WHITE)
                cnt++;
        }
        //如果此时没有与该节点相连并且颜色为白色的节点,函数返回
        if(cnt == 0) 
            return;
        //否则,将与该节点相连的、白色节点暂存至数组e中
        Edge e[] = new Edge[cnt];
        cnt = 0;
        for(Edge w = G.firstEdge(u); G.isEdge(w); w = G.nextEdge(w)){
            if(color[w.get_v2()] == COLOR.WHITE)
                e[cnt++] = w;
        }
        /*对数组e按照边的终点的访问次序ord[..]进行排序
         * 这里采用选择排序来完成这一过程 
*/
        int max_idx;
        for(int i = 0; i < e.length - 1; i++){
            //第i轮找第i大的元素
            max_idx = i;
            for(int j = i + 1; j < e.length; j++){
                if(ord[e[j].get_v2()] > ord[e[max_idx].get_v2()]){
                    max_idx = j;
                }
            }
            //如果原先第i位置上不是最大的,则交换操作
            if(max_idx != i){
                Edge t = e[i];
                e[i] = e[max_idx];
                e[max_idx] = t;
            }
        }
        //对排序后的边逐个进行递归调用
        for(int i = 0; i < e.length; i++)
            KosarajuDFSVISIT(G, ord, e[i].get_v2(), slk);
    }
复制代码

 

函数中递归搜索顶点u的相邻顶点时,首相将u的所有尚未访问的相邻顶点(边)保存至一个边数组e中。然后再对这个数组中的边进行重排序,排序的规则依然是在原图中顶点DFS搜索离开时间。图(a)的运行结果如图所示。

这里不对Kosaraju算法进行详细证明,感兴趣的读者可以参阅相关的图算法书籍。

原创粉丝点击