术语

  • 有向完全图:把所有顶点都用边连起来的图,共n(n-1)条边。
  • 简单有向路径:不含重复顶点。我们讨论的“路径”都是简单的。
  • 简单有向环:除了起终点之外不含重复顶点和边的环。
  • 邻接表中的索引是边的起点,值是边的终点。

    构造图

  • 基本和无向图相同,其中邻接表存储边的时候只需要存储一次,而addEdge(int v,int w)参数有前后之分,前一个表示起点,后一个为终点。

    /**
     * @Author haien
     * @Description Direct graph有向图
     * @Date 2018/12/4
     **/
    public class Digraph {
        //顶点数
        private  int V;
        //边数
        private int E;
        //邻接点背包数组
        private Bag<Integer>[] adj;
    
        public Digraph(int V) {
            this.V=V;
            this.E=0;
            adj=(Bag<Integer>[])new Bag[V];
            for(int i=0;i<V;i++)
                adj[i]=new Bag<Integer>();
        }
    
        public int V(){
            return V();
        }
    
        public int E(){
            return E;
        }
    
        public void addEdge(int v,int w){
            adj[v].add(w);
            E++;
        }
    
        public Iterable<Integer> adj(int v){
            return adj[v];
        }
    
        /**
         * @Author haien
         * @Description 将图中所有边的方向逆转 
         * @Date 2018/12/4
         * @Param []
         * @return void
         **/
        public Digraph reverse(){
            Digraph R=new Digraph(V);
            for(int v=0;v<V;v++){
                for(int w:adj[v])
                    R.addEdge(w,v);
            }
            return R;
        }
    }
    

    深度优先搜索

  • 和无向图几乎一样;能够判断从给定的一个点或一组点能到达哪些其他点,即单点可达性和多点可达性。

    public class DirectedDFS {
        private boolean[] marked;
    
        /**
         * @Author haien
         * @Description 单点可达性:从一个点出发能到达哪些点
         * @Date 2018/12/4
         * @Param [G, s]
         **/
        public DirectedDFS(Digraph G,int s) {
            marked=new boolean[G.V()];
            dfs(G,s);
        }
    
        /**
         * @Author haien
         * @Description 多点可达性:从一组点中任一点触发能到达哪些点
         * @Date 11:24 2018/12/4
         * @Param [G, sources]
         **/
        public DirectedDFS(Digraph G,Iterable<Integer> sources) {
            marked=new boolean[G.V()];
            for(int s:sources) {
                if (!marked[s])
                    dfs(G,s);
            }
        }
    
        private void dfs(Digraph G,int v){
            marked[v]=true;
            for(int w: G.adj(v)){
                if(!marked[w])
                    dfs(G,w);
            }
        }
    
        public boolean marked(int v) {
             return marked[v];
        }
    }
    
  • 多点可达性的一个应用是在内存管理系统中,一个顶点表示一个对象,一条边则表示一个对象对另一个对象的引用。内存管理系统会周期性地回收不可达的对象。

    拓扑排序

  • 规定一条路径中起点到终点的优先级依次递减,用这种方法实现的排序叫拓扑排序。
  • 拓扑排序中不可能存在环,因为环中的顶点根本无法分清优先级。

    /**
     * @Author haien
     * @Description 寻找图中的有向环
     * @Date 2018/12/4
     **/
    public class DirectedCycle {
        private boolean[] marked;
        //索引的上一个顶点
        private int[] lastVertex;
        //有向环中的所有顶点
        private Stack<Integer> cycle;
        //递归调用的栈上的所有顶点
        private boolean[] onStack;
    
        public DirectedCycle(Digraph G) {
            onStack=new boolean[G.V()];
            lastVertex=new int[G.V()];
            marked=new boolean[G.V()];
            for(int v=0;v<G.V();v++){
                if(!marked[v])
                    dfs(G,v);
            }
        }
    
        private void dfs(Digraph G,int v){
            onStack[v]=true;
            marked[v]=true;
            for(int w:G.adj(v)){
                if(hasCycle()) return;
                else if(!marked[w]){
                    lastVertex[w]=v;
                    dfs(G,w);
                }
                else if(onStack[w]){
                    cycle=new Stack<Integer>();
                    for(int x=v;x!=w;x=lastVertex[x])
                        cycle.push(x);
                    cycle.push(w);
                    cycle.push(v);
                }
            }
            //递归出栈后,刚找到的环all元素全变成false,直到遇到还有其他邻接点可遍历的顶点,才保持true
            onStack[v]=false;
        }
    
        public boolean hasCycle() {
            return cycle!=null;
        }
    
        public Iterable<Integer> cycle(){
            return cycle;
        }
    }
    
  • 然后我们来尝试几种排序方法,从中摸索哪一种符合拓扑排序

    /**
     * @Author haien
     * @Description 基于深搜的顶点排序
     * @Date 2018/12/5
     **/
    public class DepthFirstOrder {
        private boolean[] marked;
        //所有顶点的前序排序
        private Queue<Integer> pre;
        //后序排序
        private Queue<Integer> post;
        //逆后序排序
        private Stack<Integer> reversePost;
    
        public DepthFirstOrder(Digraph G) {
            pre=new Queue<Integer>();
            post=new Queue<Integer>();
            reversePost=new Stack<Integer>();
            marked=new boolean[G.V()];
    
            for (int v=0;v<G.V();v++){
                if (!marked[v])
                    dfs(G,v);
            }
        }
    
        private void dfs(Digraph G,int v){
            pre.enQueue(v);
    
            marked[v]=true;
            for(int w:G.adj(v)){
                if(!marked[w])
                    dfs(G,w);
            }
    
            post.enQueue(v);
    
            reversePost.push(v); //这种排序即拓扑排序
        }
    
        public Iterable<Integer> pre(){
            return pre;
        }
    
        public Iterable<Integer> post(){
            return post;
        }
    
        public Iterable<Integer> reversePost(){
            return reversePost;
        }
    }
    
  • 详细过程参考《算法》579页reversePost过程,这就是拓扑排序了
  • 拓扑排序的前提是不存在环,因此实现算法之前必须先检测环