首页 > 代码库 > 有向图—任务调度拓扑图

有向图—任务调度拓扑图

1.有向图的数据类型

使用Bag表示有向图,其中边v->w表示为顶点v所对应的邻接链表中包含一个w顶点,与无向图不同的是,这里每条边只会出现一次.有向图的数据结构类型如下:

public class Digraph {
    private final 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 v=0;v<V;v++) {
            adj[v]=new Bag<Integer>();
        }
    }
    public int V() {
        return V;
    }
    public int E() {
        return E;
    }
    //添加一条边v->w,由于是有向图只要添加一条边就可以了
    public void addEdge(int v,int w) {
        adj[v].add(w);
        E++;
    }
    public Iterable<Integer> adj(int v) {
        return adj[v];
    }
    //返回当前图的一个反向的图
    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;
    }
}

 

2.有向图中的可达性

无向图的连通性相似,同利用深度优先搜索可以解决有向图中

单点可达性问题:即:给定一幅有向图和一个起点s,回答是否存在一条从s到达给定顶点v的有向路径的问题.

多点可达性问题:给定一幅有向图和顶点的集合,回答是否存在一条从集合中的任意顶点到达给定顶点v的有向路径?

public class DirectedDFS {
    private boolean[] marked;
    //从G中找出所有s可达的点
    public DirectedDFS(Digraph G,int s) {
        marked=new boolean[G.V()];
        dfs(G,s);
    }
    //G中找出一系列点可达的点
    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);
        }
    }
    //v是可达的吗
    public boolean marked(int v) {
        return marked[v];
    }
}

多点可达性问题的一个重要时机应用是在典型的内存管理系统中,包括许多java的实现。在一个有向图中,一个顶点表示一个对象,一条边则表示一个对象对另一个对象的引用。

这个模型很好表现了运行中的java程序的内存使用状况。在程序执行的任何时候都有某些对象是可以被直接访问的,而不能通过这些对象访问到的所有对象都应该被回收以便

释放内存。它会周期性的运行一个类似于DirectedDFS的有向图可达性算法来标记所有可以被访问到的对象。

 

3.有向图的寻路

和无向图类似,有向图中常见的问题:

单点有向路径。给定一幅有向图和一个起点,回答“从s到给定目的顶点v是否存在一条有向路径?如果有,找出这条路径”

单点最短有向路径。给定一幅有向图和一个起点,回答“从s到给定目的顶点v是否存在一条有向路径,如果有,找出其中最短的那条(所含边数最少)”

 

4.调度问题—拓扑排序

4.1寻找有向环

如果一个有优先限制的问题中存在有向环,那么这个问题肯定是无解的。所以需要进行有向环的检测。

下面的代码可以用来检测给定的有向图中是否含有有向环,如果有,则按照路径的方向返回环上的所有顶点.

在执行dfs的时候,查找的是从起点到v的有向路径,onStack数组标记了递归调用的栈上的所有顶点,同时也加入了edgeTo数组,在找到有向环的时候返回环中的所有顶点.

/**
 * 有向图G是否含有有向环
 * 获取有向环中的所有顶点
 * @author Administrator
 *
 */
public class DirectedCycle {
    private boolean[] marked;    
    private int[] edgeTo;
    private Stack<Integer> cycle;    //有向环中的所有顶点
    private boolean[] onStack;        //递归调用的栈上的所有顶点
    
    public DirectedCycle(Digraph G) {
        edgeTo=new int[G.V()];
        onStack=new boolean[G.V()];
        marked=new boolean[G.V()];
        for(int v=0;v<G.V();v++) {
            if(!marked[v]) dfs(G,v);
        }
    }
/**
 * 该算法的关键步骤在于onStack数组的运用.
 * onStack数组标记的是当前遍历的点.如果对于一个点指向的所有点中的某个点
 * onstack[v]=true.代表该点正在被遍历也就是说
 * 该点存在一条路径,指向这个点.而这个点现在又可以指向该点,
 * 即存在环的结构~
 * @param G
 * @param v
 */ 
    private void dfs(Digraph G, int v) {
        onStack[v]=true;
        marked[v]=true;
        for(int w:G.adj(v)) {
            if(this.hasCycle()) return;
            else if(!marked[w]) {
                edgeTo[w]=v;
                dfs(G,w);
            }
            else if(onStack[w]) {
                cycle=new Stack<Integer>();
                for(int x=v;x!=w;x=edgeTo[x])
                    cycle.push(x);
                cycle.push(w);
                cycle.push(v);
            }
        }
        //dfs方法结束,对于该点的递归调用结束.该点指向的所有点已经遍历完毕
        onStack[v]=false;
    }

    private boolean hasCycle() {
        return cycle!=null;
    }
    public Iterable<Integer> cycle() {
        return cycle;
    }
}

 

4.2 拓扑排序

拓补排序:给定一幅有向图,将所有的顶点排序,使得所有的有向边均从排在前面的元素指向排在后面的元素.如果存在有向环的话,那么拓补排序无法完成.

  要实现有向图的拓补排序,利用标准深度优先搜索顺序即可完成任务.这里顶点会有三种排列顺序:

  1.前序:在递归调用前将顶点加入队列

  2.后序:在递归调用之后将顶点加入队列

  3.逆后序:在递归调用之后将顶点压入栈.

  具体的操作见下面的代码:

//有向图中基于深度优先搜索的拓补排序
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<>();
        post=new Queue<>();
        reversePost=new Stack<>();
        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;
    }
}

遍历的顺序取决于这个数据结构的性质以及是在递归调用之前还是之后进行保存。

前序:在递归调用之前将顶点加入队列。

后序:在递归调用之后将顶点加入队列。

逆后序:在递归调用之后将顶点压入栈。

 

前序就时dfs()的调用顺序;后序就是顶点遍历完成的顺序;逆后序就是顶点遍历完成顺序的逆。

拓补排序的实现依赖于上面的API,实际上拓补排序即为所有顶点的逆后序排列

拓补排序的代码如下:

public class Topological {
    private Iterable<Integer> order;    //顶点的拓补排序
    public Topological(Digraph G) {
        DirectedCycle cyclefinder=new DirectedCycle(G);
        if(!cyclefinder.hasCycle()) {//只有无环才能进行拓补排序
            DepthFirstOrder dfs=new DepthFirstOrder(G);
            order=dfs.reversePost();
        }
    }
    public Iterable<Integer> order() {
        return order;
    }
    public boolean isDAG() {
        return order!=null;
    }
}

 

有向图—任务调度拓扑图