算法: 有向无环图(DAG)的拓扑排序

更新:
拓扑排序有2中方法(最后结果可能不同,因为拓扑排序有多解)。
一个简单的求拓扑排序的算法是先找出任意一个没有入边的顶点,然后将它和它的边从图中删除。然后对剩余部分使用同样的操作。

    public ArrayList<Integer> topo()
    {
        ArrayList<Integer> result = new ArrayList<>();
        Queue<Integer> que = new LinkedList<>();
        int[] ind = new int[V];
        for(int i=0;i<V;i++)
            ind[i] = 0;
        for(LinkedList<Integer> nodes:adj)
            for(int x:nodes)
                ind[x]++;

        for(int i=0;i<V;i++)
            if(ind[i]==0)
                que.offer(i);

        while(!que.isEmpty())
        {
            int k = que.poll();
            result.add(k);
            for(int x:adj(k))
            {
                ind[x]--;
                if(ind[x]==0)
                    que.offer(x);
            }
        }
        return result;
    }

定义: 拓扑排序是对有向无环图(DAG)的顶点的一种排序, 使得如果存在一条从v到w的路径,那么在排序中w就出现在v的后面。
如果图含有环,那么拓扑排序是不可能的。试想有3个正整数,a比b大,b比c大,c比a大,我们无法对abc排序。

算法:
1. 一个简单的求拓扑排序的算法是先找出任意一个没有入边的顶点,然后将它和它的边从图中删除。然后对剩余部分使用同样的操作。
2. 另一种不那么直观,却更加简单的算法是求所有顶点的逆后序排列

第二种方法:

所以我们要做的就是:
先判断该图是不是一幅有向无环图,如果是,则进行拓扑排序。在这个过程中,我们需要用dfs遍历该图2遍

性能分析:
使用深度优先搜索对有向无环图进行拓扑排序所需的时间和V+E成正比。

检测有向图中是否有环:

用深度优先搜索来解决这个问题并不困难,一旦我们找到了一条有向边v->w且w已经存在于栈中,就找到了一个环。

实现中使用了一个onStack[]数组,以标记递归调用的栈上的所有顶点。而路径信息依然可以通过from[]数组得到。

下例是个简单实现,只能保存遍历过程中检测到的最后一条环路。但已经足够了,代码修改起来也十分简单。

package DiGraphs;

import java.util.Stack;

public class CycleDetect {

    private DiGraph g;//有向图
    private boolean[] marked;//是否被访问过
    private Stack<Integer> cycle;//存放有向环路径的栈
    private boolean[] onStack;//判断索引位置的顶点是否在栈中
    private int from[];//保存路径信息


    public CycleDetect(DiGraph g) {
        this.g = g;
        marked = new boolean[g.V()];
        onStack = new boolean[g.V()];
        from = new int[g.V()];
    }

    /** * 遍历有向图,对所有连同分量都进行深度优先搜索 */
    public void dfs(){
        for (int v = 0; v < g.V(); v++)
            if (!marked(v))
                dfs(g, v);
    }

    /** * 深度优先搜索,将路径信息保存在from数组。如果发现有环,则将路径放入cycle栈中。 * @param g * @param s */
    private void dfs(DiGraph g, int s) {
        marked[s] = true;
        onStack[s] = true;
        for (int w : g.adj(s))
            if (this.hasCycle())
                return;
            else if (!marked(w)) {
                from[w] = s;
                dfs(g, w);
            } else if (onStack[w]) {//如果已被访问且在栈内
                cycle = new Stack<Integer>();
                for (int x = s; x != w; x = from[x])
                    cycle.push(x);//将路径逆序push进栈中
                cycle.push(w);
                cycle.push(s);
            }
        onStack[s] = false;
    }

    public boolean hasCycle() {
        return cycle != null;
    }

    public Iterable<Integer> cycle(){
        return cycle;
    }

    private boolean marked(int w) {
        return marked[w];
    }



    public static void main(String[] args) {
        DiGraph g = new DiGraph(13);
        g.addEdge(0, 5);
        g.addEdge(5, 4);
        g.addEdge(4, 3);
        g.addEdge(3, 5);
        g.addEdge(5, 0);

        CycleDetect detectCycle = new CycleDetect(g);
        detectCycle.dfs();

        Stack<Integer> st = (Stack<Integer>) detectCycle.cycle();
        while(!st.isEmpty()){
            System.out.print(st.pop() + " ");
        }
    }
    //result: 3 5 4 3 
}

基于DFS的顶点排序

人们一般对3种排序感兴趣:
前序: 在递归调用之前将顶点加入队列。前序就是dfs()的调用顺序。
后序: 在递归调用之后将顶点加入队列。后序就是顶点遍历完成的顺序。
逆后序: 在递归调用之后将顶点压入栈。逆后序就是后序的逆序。

它们的实现都非常简单,在原本的dfs()方法中添加3行代码就能实现:

    public void dfs(DiGraph g, int v) {
        pre.enQueue(v);//pre是MyQueue类型的
        marked[s] = true;
        for(int w:g.adj(v)){
            if(!marked(w))
                dfs(g,w);
        }
        post.enQueue(v);
        reversePost.push(v);//MyArrayStack类型
    }

    public Iterable<Integer> pre(){
        return pre;
    }
    public Iterable<Integer> post(){
        return post;
    }
    public Iterable<Integer> reversePost(){
        return reversePost;
    }

拓扑排序

《算法: 有向无环图(DAG)的拓扑排序》

package DiGraphs;

import java.util.Stack;

public class Topological {

    private DiGraph g;
    private Iterable<Integer> order;// 拓扑排序序列

    public Topological(DiGraph g) {
        this.g = g;
    }

    /** * 返回拓扑排序序列,如果不存在,返回null * * @return */
    public Iterable<Integer> order() {
        CycleDetect cd = new CycleDetect(g);
        if (!cd.hasCycle()) {// 如果没有环,就排序
            DiDFSOrder dfsOrder = new DiDFSOrder(g);
            dfsOrder.dfs();
            order = dfsOrder.reversePost();// dfs的逆后序
        }
        return order;
    }

    public static void main(String[] args) {
        DiGraph g = new DiGraph(8);
        g.addEdge(0, 5);
        g.addEdge(0, 2);
        g.addEdge(0, 4);
        g.addEdge(5, 4);
        g.addEdge(5, 6);
        g.addEdge(2, 3);
        g.addEdge(3, 5);
        g.addEdge(4, 7);
        g.addEdge(6, 1);

        Topological t = new Topological(g);

        Stack<Integer> s = new Stack<Integer>();
        s = (Stack<Integer>) t.order();
        while (!s.isEmpty())
            System.out.print(s.pop() + " ");
        //result: 0 2 3 5 6 1 4 7 正确
    }
}

总结

拓扑排序并不是一个困难的算法,在后面的加权有向图的最短路径问题中还会遇到它。

    原文作者:排序算法
    原文地址: https://blog.csdn.net/u012674931/article/details/49334129
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞