有向图—任务调度拓扑图


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; } }

 

5.有向图的强连通性

定义:如果两个顶点v和w是互相可达的,则称它们为强连通的.也就是说既存在一条从v到w的有向路径也存在一条从w到v的有向路径.

如果一幅有向图中的任意两个顶点都是强连通的,则称这副有向图也是强连通的.任意顶点和自己都是强连通的.

下面的代码采用如下步骤来计算强连通分量以及两个点是否是强连通的:

  1.在给定的有向图中,使用DepthFirsetOrder来计算它的反向图GR的逆后序排列

  2.按照第一步计算得到的顺序采用深度优先搜索来访问所有未被标记的点

  3.在构造函数中,所有在同一个递归dfs()调用中被访问到的顶点都是在同一个强连通分量中.

  下面的代码实现遵循了上面的思路:

/**
 * 该算法实现的关键:
 * 使用深度优先搜索查找给定有向图的反向图GR.根据由此得到的所有顶点的逆后序
 * 再次用深度优先搜索处理有向图G.其构造函数的每一次递归调用所标记的顶点都在
 * 同一个强连通分量中.
 * 解决问题:
 * 判断两个点是否是强连通的
 * 判断总共有多少个连通分量
 * @author Administrator
 *
 */
public class KosarajuSCC {
    private boolean[] marked;//已经访问过的顶点
    private int[] id;        //强连通分量的标识符
    private int count;        //强联通分量的数量
    public KosarajuSCC(Digraph G) {
        marked=new boolean[G.V()];
        id=new int[G.V()];
        DepthFirstOrder order=new DepthFirstOrder(G.reverse());
        for(int s:order.reversePost()) {
            if(!marked[s]) {
                dfs(G,s);
                count++;
            }
        }
    }
    private void dfs(Digraph G, int v) {
        marked[v]=true;
        id[v]=count;
        for(int w:G.adj(v)) {
            if(!marked[w]) {
                dfs(G,w);
            }
        }
    }
    public boolean stronglyConnected(int v,int w) {
        return id[v]==id[w];
    }
    public int id(int v) {
        return id[v];
    }
    public int count() {
        return count;
    }
}

 


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM