小浊微清的博客@IT·互联网互联网科技

SCC算法初解

2017-05-19  本文已影响87人  小浊微清

在算法学习之路上漂泊,遇见了图,而分无向与有向。在本文中主要讲解关于有向图中的求极大连通分量的算法,主要是Kasaraju算法、Tarjan算法以及Gabow算法。

三种算法都是基于深度优先搜索算法(DFS)而实现的,实际上后两种算法是对于Kasaraju算法的改进,减少了一次深度优先搜索(DFS),因此在性能上相比较而言要好一些。

初识强连通分量

首先,连通分量是无向图G的一个极大连通子图称为G的一个连通分量(或连通分支)。连通图只有一个连通分量,即其自身;非连通的无向图有多个连通分量。

强连通图指每一个顶点皆可以经由该图上的边抵达其他的每一个点的有向图。意即对于此图上每一个点对(Va,Vb),皆存在路径Va→Vb以及Vb→Va。强连通分量则是指一张有向图G的极大强连通子图G'。如果将每一个强连通分量缩成一个点,则原图G将会变成一张有向无环图。一张图被称为有向无环图当且仅当此图不具有点集合数量大于一的强连通分量,因为有向环即是一个强连通分量,而且任何的强连通分量皆具有至少一个有向环。(摘自维基百科)

对于无向图,求连通分量的问题就等价于求是否连通的问题,使用深度优先、广度优先搜索的算法的到的树都能求出最大连通分量。

Kasaraju算法

Kasaraju算法在我第一次接触时,感觉确实有点难理解,虽然现在也还是有点难理解。本文中不会去证明算法,只是讲解算法的一些实现等。

public static class KosarajuSCC {
        int n;
        List<Integer>[] adj;

        KosarajuSCC(int n) {
            this.n = n;
            this.adj = new ArrayList[n];
            for (int i = 0; i < n; i++) {
                this.adj[i] = new ArrayList<>();
            }
        }

        public void addEdge(int v, int w) {
            this.adj[v].add(w);
        }

        //正向遍历,以后根序压栈,保证根先出栈
        public void fillorder(int v, boolean[] visited, Stack<Integer> s) {
            visited[v] = true;
            for (Integer i : this.adj[v]) {
                if (!visited[i]) {
                    fillorder(i, visited, s);
                }
            }
            s.push(v);
        }

        //reverse 得到反向图
        public KosarajuSCC getTranspose() {
            KosarajuSCC gv = new KosarajuSCC(this.n);
            for (int i = 0; i < n; i++) {
                for (Integer j : this.adj[i]) {
                    gv.adj[j].add(i);
                }
            }
            return gv;
        }

        //DFS打印连通分支
        public void DFSUtil(int v, boolean[] visited) {
            visited[v] = true;
            System.out.print(v + " ");
            for (Integer i : adj[v]) {
                if (!visited[i]) {
                    DFSUtil(i, visited);
                }
            }
        }

        //按照Kosaraju算法的步骤执行
        public void printSCCs() {
            Stack<Integer> s = new Stack<Integer>();
            boolean[] visited = new boolean[this.n];
            for (int i = 0; i < n; i++) {
                visited[i] = false;
            }
            //逆后序压栈
            for (int i = 0; i < n; i++) {
                if (!visited[i]) {
                    fillorder(i, visited, s);
                }
            }
            //得到反向图
            KosarajuSCC gr = this.getTranspose();
            for (int i = 0; i < n; i++) {
                visited[i] = false;
            }
            //依据反向图算可达性
            while (!s.empty()) {
                int v = s.pop();
                if (visited[v] == false) {
                    gr.DFSUtil(v, visited);
                    System.out.println();
                }
            }
        }

    }

先理解一下Karasaju算法的思路。

Kasaraju算法 示例图G

如上图示例的有向图,先求逆后序排序,得到{7, 8, 6, 9, 11, 10, 12, 0, 5, 4, 2, 3, 1},然后按照这个图的转置图GR进行DFS,最终可以得到极大强连通分量5个:{7, 8}, {6}, {9, 11, 10, 12}, {0, 5, 4, 2, 3}, {1}

在Karasaju算法中使用了两次DFS,第一次是得到节点的逆后序排序(有的算法书将逆后序排序合并在拓扑排序里面);第二次是对于转置图DFS得到最终的强连通分量。我们当然想要对于算法进行优化,减少DFS的次数也是一种极好的优化方式,想想如果一次DFS就可以得出强连通分量岂不是很好。

Tarjan算法

Tarjan算法是对于Kasaraju算法的改进。其基本代码实现思维如下:

Tarjan算法
public static class TarjanSCC {
        private int numOfNode;
        private List<ArrayList<Integer>> graph;//二维数组表示图
        private List<ArrayList<Integer>> result;//保存极大强连通图
        private boolean[] inStack;//标记节点是否在栈内
        private Stack<Integer> stack;
        private int[] dfn;
        private int[] low;
        private int time;//当前时间戳(实际是一个int的数,标记当前访问的节点)
        private static final int NO_VISIT = 0;

        public TarjanSCC(List<ArrayList<Integer>> graph, int numOfNode) {
            this.graph = graph;
            this.numOfNode = numOfNode;
            this.inStack = new boolean[numOfNode];
            this.stack = new Stack<Integer>();
            dfn = new int[numOfNode];
            low = new int[numOfNode];

            Arrays.fill(dfn, NO_VISIT);//将dfn所有元素都置为0,代表i还有没被访问过。
            Arrays.fill(low, NO_VISIT);
            result = new ArrayList<ArrayList<Integer>>();
        }

        //获取强连通分量
        public List<ArrayList<Integer>> tarjanResult() {
            for (int i = 0; i < numOfNode; i++) {
                if (dfn[i] == NO_VISIT) {
                    tarjan(i);
                }
            }
            return result;
        }

        //算法核心
        public void tarjan(int current) {
            dfn[current] = low[current] = time++;
            inStack[current] = true;
            stack.push(current);

            for (int i = 0; i < graph.get(current).size(); i++) {
                int next = graph.get(current).get(i);
                if (dfn[next] == NO_VISIT) {
                    tarjan(next);
                    low[current] = Math.min(low[current], low[next]);
                } else if (inStack[next]) {
                    low[current] = Math.min(low[current], dfn[next]);
                }
            }

            if (low[current] == dfn[current]) {
                ArrayList<Integer> temp = new ArrayList<Integer>();
                int j = -1;
                while (current != j) {
                    j = stack.pop();
                    inStack[j] = false;
                    temp.add(j);
                }
                result.add(temp);
            }
        }

    }

需要注意的是在算法中的时间戳这个标记,并不是代表真正的时间戳,而是对于每个节点不同的一种标记,在本文算法中都是用一个递增数组来表示,即访问每个节点时,将该时间戳变量自增赋值给该节点的时间戳DFN[i]

Gabow算法

Gabow算法在基础上与Tarjan算法相似,都是利用一次DFS算法实现。

public static class GabowSCC {
        private int numOfNode;
        private List<ArrayList<Integer>> graph;//二维数组表示图
        private List<ArrayList<Integer>> result;//保存极大强连通图
        private Stack<Integer> path;
        private Stack<Integer> root;
        private int[] order;
        private int time;//当前时间戳(实际是一个int的数,标记当前访问的节点)
        private static final int NO_VISIT = -1;
        private int[] part; // 连通变量的标号;
        private int partNum = 0;

        public GabowSCC(List<ArrayList<Integer>> graph, int numOfNode) {
            this.graph = graph;
            this.numOfNode = numOfNode;
            this.path = new Stack<>();
            this.root = new Stack<>();
            order = new int[numOfNode];
            part = new int[numOfNode];

            Arrays.fill(order, NO_VISIT);
            Arrays.fill(part, NO_VISIT);
        }

        public int[] gabowResult() {
            for (int i = 0; i < numOfNode; i++) {
                if (order[i] == NO_VISIT) {
                    gabow(i);
                }
            }
            return part;
        }

        public void gabow(int v) {
            order[v] = ++time;
            path.push(v);
            root.push(v);

            for (int i = 0; i < graph.get(v).size(); i++) {
                int next = graph.get(v).get(i);
                if (order[next] == NO_VISIT) {
                    gabow(next);
                } else if (part[next] == NO_VISIT) {
                    while (order[root.peek()] > order[next]) {
                        root.pop();
                    }
                }
            }

            if (v == root.peek()) {
                root.pop();
                partNum++;
                int top;
                do {
                    top = path.peek();
                    part[top] = partNum;
                    path.pop();
                } while (top != v);
            }
        }

    }

其算法基本思路是:

Gabow算法

本文只涉及算法的实现,没有设计算法的证明等,如有想法,请分享。

上一篇 下一篇

猜你喜欢

热点阅读